mirror of https://github.com/halo-dev/halo
Refactor plugin reconciliation for dependency mechanism (#5900)
#### What type of PR is this? /kind improvement /area core /area plugin #### What this PR does / why we need it: This PR wholly refactors plugin reconciliation to implement dependency mechanism. Currently, - If we disable plugin which has dependents, the plugin must wait for dependents to be disabled. - If we enable plugin which has dependencies , the plugin must wait for dependencies to be enabled. - If we upgrade plugin which has dependents, the plugin must request dependents to be unloaded. After the plugin is unloaded, the plugin must cancel unload request for dependents. #### Which issue(s) this PR fixes: Fixes #5872 #### Special notes for your reviewer: #### Does this PR introduce a user-facing change? ```release-note 优化被依赖的插件的升级,启用和禁用 ```pull/5994/head^2
parent
769b19c23c
commit
5df51bb715
|
|
@ -14089,8 +14089,6 @@
|
||||||
"Condition": {
|
"Condition": {
|
||||||
"required": [
|
"required": [
|
||||||
"lastTransitionTime",
|
"lastTransitionTime",
|
||||||
"message",
|
|
||||||
"reason",
|
|
||||||
"status",
|
"status",
|
||||||
"type"
|
"type"
|
||||||
],
|
],
|
||||||
|
|
@ -16944,6 +16942,7 @@
|
||||||
"PENDING",
|
"PENDING",
|
||||||
"STARTING",
|
"STARTING",
|
||||||
"CREATED",
|
"CREATED",
|
||||||
|
"DISABLING",
|
||||||
"DISABLED",
|
"DISABLED",
|
||||||
"RESOLVED",
|
"RESOLVED",
|
||||||
"STARTED",
|
"STARTED",
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,7 @@ public class Plugin extends AbstractExtension {
|
||||||
PENDING,
|
PENDING,
|
||||||
STARTING,
|
STARTING,
|
||||||
CREATED,
|
CREATED,
|
||||||
|
DISABLING,
|
||||||
DISABLED,
|
DISABLED,
|
||||||
RESOLVED,
|
RESOLVED,
|
||||||
STARTED,
|
STARTED,
|
||||||
|
|
|
||||||
|
|
@ -51,13 +51,15 @@ public class Condition {
|
||||||
* Human-readable message indicating details about last transition.
|
* Human-readable message indicating details about last transition.
|
||||||
* This may be an empty string.
|
* This may be an empty string.
|
||||||
*/
|
*/
|
||||||
@Schema(requiredMode = REQUIRED, maxLength = 32768)
|
@Schema(maxLength = 32768)
|
||||||
private String message;
|
@Builder.Default
|
||||||
|
private String message = "";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unique, one-word, CamelCase reason for the condition's last transition.
|
* Unique, one-word, CamelCase reason for the condition's last transition.
|
||||||
*/
|
*/
|
||||||
@Schema(requiredMode = REQUIRED, maxLength = 1024,
|
@Schema(maxLength = 1024,
|
||||||
pattern = "^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$")
|
pattern = "^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$")
|
||||||
private String reason;
|
@Builder.Default
|
||||||
|
private String reason = "";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,13 +55,33 @@ public class ConditionList extends AbstractCollection<Condition> {
|
||||||
* @param condition item to add
|
* @param condition item to add
|
||||||
*/
|
*/
|
||||||
public boolean addAndEvictFIFO(@NonNull Condition condition, int evictThreshold) {
|
public boolean addAndEvictFIFO(@NonNull Condition condition, int evictThreshold) {
|
||||||
boolean result = this.addFirst(condition);
|
var current = getCondition(condition.getType());
|
||||||
|
if (current != null) {
|
||||||
|
// do not update last transition time if status is not changed
|
||||||
|
if (Objects.equals(condition.getStatus(), current.getStatus())) {
|
||||||
|
condition.setLastTransitionTime(current.getLastTransitionTime());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conditions.remove(current);
|
||||||
|
conditions.addFirst(condition);
|
||||||
|
|
||||||
while (conditions.size() > evictThreshold) {
|
while (conditions.size() > evictThreshold) {
|
||||||
removeLast();
|
removeLast();
|
||||||
}
|
}
|
||||||
return result;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Condition getCondition(String type) {
|
||||||
|
for (Condition condition : conditions) {
|
||||||
|
if (condition.getType().equals(type)) {
|
||||||
|
return condition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public void remove(Condition condition) {
|
public void remove(Condition condition) {
|
||||||
conditions.remove(condition);
|
conditions.remove(condition);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -284,51 +284,8 @@ public class PluginEndpoint implements CustomEndpoint {
|
||||||
return request.bodyToMono(RunningStateRequest.class)
|
return request.bodyToMono(RunningStateRequest.class)
|
||||||
.flatMap(runningState -> {
|
.flatMap(runningState -> {
|
||||||
var enable = runningState.isEnable();
|
var enable = runningState.isEnable();
|
||||||
var updatedPlugin = Mono.defer(() -> client.get(Plugin.class, name))
|
|
||||||
.flatMap(plugin -> {
|
|
||||||
if (!Objects.equals(enable, plugin.getSpec().getEnabled())) {
|
|
||||||
plugin.getSpec().setEnabled(enable);
|
|
||||||
log.debug("Updating plugin {} state to {}", name, enable);
|
|
||||||
return client.update(plugin);
|
|
||||||
}
|
|
||||||
log.debug("Checking plugin {} state, no need to update", name);
|
|
||||||
return Mono.just(plugin);
|
|
||||||
});
|
|
||||||
|
|
||||||
var async = runningState.isAsync();
|
var async = runningState.isAsync();
|
||||||
if (!async) {
|
return pluginService.changeState(name, enable, !async);
|
||||||
// if we want to wait the state of plugin to be updated
|
|
||||||
updatedPlugin = updatedPlugin
|
|
||||||
.flatMap(plugin -> {
|
|
||||||
var phase = plugin.statusNonNull().getPhase();
|
|
||||||
if (enable) {
|
|
||||||
// if we request to enable the plugin
|
|
||||||
if (!(Plugin.Phase.STARTED.equals(phase)
|
|
||||||
|| Plugin.Phase.FAILED.equals(phase))) {
|
|
||||||
return Mono.error(UnexpectedPluginStateException::new);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// if we request to disable the plugin
|
|
||||||
if (Plugin.Phase.STARTED.equals(phase)) {
|
|
||||||
return Mono.error(UnexpectedPluginStateException::new);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Mono.just(plugin);
|
|
||||||
})
|
|
||||||
.retryWhen(
|
|
||||||
Retry.backoff(10, Duration.ofMillis(100))
|
|
||||||
.filter(UnexpectedPluginStateException.class::isInstance)
|
|
||||||
.doBeforeRetry(signal ->
|
|
||||||
log.debug("Waiting for plugin {} to meet expected state", name)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.doOnSuccess(plugin -> {
|
|
||||||
log.info("Plugin {} met expected state {}",
|
|
||||||
name, plugin.statusNonNull().getPhase());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatedPlugin;
|
|
||||||
})
|
})
|
||||||
.flatMap(plugin -> ServerResponse.ok().bodyValue(plugin));
|
.flatMap(plugin -> ServerResponse.ok().bodyValue(plugin));
|
||||||
}
|
}
|
||||||
|
|
@ -883,7 +840,4 @@ public class PluginEndpoint implements CustomEndpoint {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class UnexpectedPluginStateException extends IllegalStateException {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,12 @@ import static run.halo.app.extension.ExtensionUtil.removeFinalizers;
|
||||||
import static run.halo.app.extension.MetadataUtil.nullSafeAnnotations;
|
import static run.halo.app.extension.MetadataUtil.nullSafeAnnotations;
|
||||||
import static run.halo.app.plugin.PluginConst.PLUGIN_PATH;
|
import static run.halo.app.plugin.PluginConst.PLUGIN_PATH;
|
||||||
import static run.halo.app.plugin.PluginConst.RELOAD_ANNO;
|
import static run.halo.app.plugin.PluginConst.RELOAD_ANNO;
|
||||||
import static run.halo.app.plugin.PluginConst.RUNTIME_MODE_ANNO;
|
import static run.halo.app.plugin.PluginConst.REQUEST_TO_UNLOAD_LABEL;
|
||||||
import static run.halo.app.plugin.PluginExtensionLoaderUtils.isSetting;
|
import static run.halo.app.plugin.PluginExtensionLoaderUtils.isSetting;
|
||||||
import static run.halo.app.plugin.PluginExtensionLoaderUtils.lookupExtensions;
|
import static run.halo.app.plugin.PluginExtensionLoaderUtils.lookupExtensions;
|
||||||
import static run.halo.app.plugin.PluginUtils.generateFileName;
|
import static run.halo.app.plugin.PluginUtils.generateFileName;
|
||||||
import static run.halo.app.plugin.PluginUtils.isDevelopmentMode;
|
import static run.halo.app.plugin.PluginUtils.isDevelopmentMode;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
|
|
@ -24,17 +23,17 @@ import java.nio.file.Paths;
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.function.Supplier;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.pf4j.PluginRuntimeException;
|
import org.pf4j.PluginDependency;
|
||||||
import org.pf4j.PluginState;
|
import org.pf4j.PluginState;
|
||||||
import org.pf4j.PluginStateEvent;
|
|
||||||
import org.pf4j.PluginStateListener;
|
|
||||||
import org.pf4j.PluginWrapper;
|
import org.pf4j.PluginWrapper;
|
||||||
import org.pf4j.RuntimeMode;
|
import org.pf4j.RuntimeMode;
|
||||||
import org.springframework.core.io.DefaultResourceLoader;
|
import org.springframework.core.io.DefaultResourceLoader;
|
||||||
|
|
@ -57,9 +56,8 @@ import run.halo.app.extension.controller.Reconciler;
|
||||||
import run.halo.app.extension.controller.Reconciler.Request;
|
import run.halo.app.extension.controller.Reconciler.Request;
|
||||||
import run.halo.app.extension.controller.RequeueException;
|
import run.halo.app.extension.controller.RequeueException;
|
||||||
import run.halo.app.infra.Condition;
|
import run.halo.app.infra.Condition;
|
||||||
|
import run.halo.app.infra.ConditionList;
|
||||||
import run.halo.app.infra.ConditionStatus;
|
import run.halo.app.infra.ConditionStatus;
|
||||||
import run.halo.app.infra.utils.JsonParseException;
|
|
||||||
import run.halo.app.infra.utils.JsonUtils;
|
|
||||||
import run.halo.app.infra.utils.PathUtils;
|
import run.halo.app.infra.utils.PathUtils;
|
||||||
import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
||||||
import run.halo.app.plugin.PluginConst;
|
import run.halo.app.plugin.PluginConst;
|
||||||
|
|
@ -77,8 +75,12 @@ import run.halo.app.plugin.SpringPluginManager;
|
||||||
@Component
|
@Component
|
||||||
public class PluginReconciler implements Reconciler<Request> {
|
public class PluginReconciler implements Reconciler<Request> {
|
||||||
private static final String FINALIZER_NAME = "plugin-protection";
|
private static final String FINALIZER_NAME = "plugin-protection";
|
||||||
private static final String DEPENDENTS_ANNO_KEY = "plugin.halo.run/dependents-snapshot";
|
|
||||||
|
private static final Set<String> UNUSED_ANNOTATIONS =
|
||||||
|
Set.of("plugin.halo.run/dependents-snapshot");
|
||||||
|
|
||||||
private final ExtensionClient client;
|
private final ExtensionClient client;
|
||||||
|
|
||||||
private final SpringPluginManager pluginManager;
|
private final SpringPluginManager pluginManager;
|
||||||
|
|
||||||
private final PluginProperties pluginProperties;
|
private final PluginProperties pluginProperties;
|
||||||
|
|
@ -91,9 +93,6 @@ public class PluginReconciler implements Reconciler<Request> {
|
||||||
this.pluginManager = pluginManager;
|
this.pluginManager = pluginManager;
|
||||||
this.pluginProperties = pluginProperties;
|
this.pluginProperties = pluginProperties;
|
||||||
this.clock = Clock.systemUTC();
|
this.clock = Clock.systemUTC();
|
||||||
|
|
||||||
this.pluginManager.addPluginStateListener(new PluginStartedListener());
|
|
||||||
this.pluginManager.addPluginStateListener(new PluginStoppedListener());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -124,47 +123,69 @@ public class PluginReconciler implements Reconciler<Request> {
|
||||||
return Result.doNotRetry();
|
return Result.doNotRetry();
|
||||||
}
|
}
|
||||||
addFinalizers(plugin.getMetadata(), Set.of(FINALIZER_NAME));
|
addFinalizers(plugin.getMetadata(), Set.of(FINALIZER_NAME));
|
||||||
plugin.statusNonNull().setPhase(Plugin.Phase.PENDING);
|
removeUnusedAnnotations(plugin);
|
||||||
|
|
||||||
// Prepare
|
var status = plugin.getStatus();
|
||||||
try {
|
if (status == null) {
|
||||||
resolveLoadLocation(plugin);
|
status = new Plugin.PluginStatus();
|
||||||
|
plugin.setStatus(status);
|
||||||
loadOrReload(plugin);
|
|
||||||
createOrUpdateSetting(plugin);
|
|
||||||
createOrUpdateReverseProxy(plugin);
|
|
||||||
resolveStaticResources(plugin);
|
|
||||||
|
|
||||||
if (requestToEnable(plugin)) {
|
|
||||||
// Start
|
|
||||||
enablePlugin(plugin);
|
|
||||||
} else {
|
|
||||||
// stop the plugin and disable it
|
|
||||||
disablePlugin(plugin);
|
|
||||||
}
|
}
|
||||||
} catch (Throwable t) {
|
// reset phase to pending
|
||||||
// populate condition
|
status.setPhase(Plugin.Phase.PENDING);
|
||||||
var condition = Condition.builder()
|
// init condition list if not exists
|
||||||
.type(PluginState.FAILED.toString())
|
if (status.getConditions() == null) {
|
||||||
.reason("UnexpectedState")
|
status.setConditions(new ConditionList());
|
||||||
.message(t.getMessage())
|
}
|
||||||
|
|
||||||
|
var steps = new LinkedList<Supplier<Result>>();
|
||||||
|
steps.add(() -> resolveLoadLocation(plugin));
|
||||||
|
steps.add(() -> loadOrReload(plugin));
|
||||||
|
steps.add(() -> createOrUpdateSetting(plugin));
|
||||||
|
steps.add(() -> createOrUpdateReverseProxy(plugin));
|
||||||
|
steps.add(() -> resolveStaticResources(plugin));
|
||||||
|
if (requestToEnable(plugin)) {
|
||||||
|
steps.add(() -> enablePlugin(plugin));
|
||||||
|
} else {
|
||||||
|
steps.add(() -> disablePlugin(plugin));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result result = null;
|
||||||
|
try {
|
||||||
|
for (var step : steps) {
|
||||||
|
result = step.get();
|
||||||
|
if (result != null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (Exception e) {
|
||||||
|
status.getConditions().addAndEvictFIFO(Condition.builder()
|
||||||
|
.type(ConditionType.READY)
|
||||||
.status(ConditionStatus.FALSE)
|
.status(ConditionStatus.FALSE)
|
||||||
|
.reason(ConditionReason.SYSTEM_ERROR)
|
||||||
|
.message(e.getMessage())
|
||||||
.lastTransitionTime(clock.instant())
|
.lastTransitionTime(clock.instant())
|
||||||
.build();
|
.build());
|
||||||
var status = plugin.statusNonNull();
|
status.setPhase(Plugin.Phase.UNKNOWN);
|
||||||
nullSafeConditions(status).addAndEvictFIFO(condition);
|
throw e;
|
||||||
status.setPhase(Plugin.Phase.FAILED);
|
|
||||||
throw t;
|
|
||||||
} finally {
|
} finally {
|
||||||
syncPluginState(plugin);
|
var pw = pluginManager.getPlugin(plugin.getMetadata().getName());
|
||||||
|
if (pw != null) {
|
||||||
|
status.setLastProbeState(pw.getPluginState());
|
||||||
|
}
|
||||||
client.update(plugin);
|
client.update(plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.doNotRetry();
|
|
||||||
})
|
})
|
||||||
.orElseGet(Result::doNotRetry);
|
.orElseGet(Result::doNotRetry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void removeUnusedAnnotations(Plugin plugin) {
|
||||||
|
var annotations = plugin.getMetadata().getAnnotations();
|
||||||
|
if (annotations != null) {
|
||||||
|
UNUSED_ANNOTATIONS.forEach(annotations::remove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean checkDependents(Plugin plugin) {
|
private boolean checkDependents(Plugin plugin) {
|
||||||
var pluginId = plugin.getMetadata().getName();
|
var pluginId = plugin.getMetadata().getName();
|
||||||
var dependents = pluginManager.getDependents(pluginId);
|
var dependents = pluginManager.getDependents(pluginId);
|
||||||
|
|
@ -173,17 +194,20 @@ public class PluginReconciler implements Reconciler<Request> {
|
||||||
}
|
}
|
||||||
var status = plugin.statusNonNull();
|
var status = plugin.statusNonNull();
|
||||||
var condition = Condition.builder()
|
var condition = Condition.builder()
|
||||||
.type(PluginState.FAILED.toString())
|
.type(ConditionType.PROGRESSING)
|
||||||
.reason("DependentsExist")
|
.status(ConditionStatus.UNKNOWN)
|
||||||
|
.reason(ConditionReason.WAIT_FOR_DEPENDENTS_DELETED)
|
||||||
.message(
|
.message(
|
||||||
"The plugin has dependents %s, please delete them first."
|
"The plugin has dependents %s, please delete them first."
|
||||||
.formatted(dependents.stream().map(PluginWrapper::getPluginId).toList())
|
.formatted(dependents.stream().map(PluginWrapper::getPluginId).toList())
|
||||||
)
|
)
|
||||||
.status(ConditionStatus.FALSE)
|
|
||||||
.lastTransitionTime(clock.instant())
|
.lastTransitionTime(clock.instant())
|
||||||
.build();
|
.build();
|
||||||
nullSafeConditions(status).addAndEvictFIFO(condition);
|
var conditions = nullSafeConditions(status);
|
||||||
status.setPhase(Plugin.Phase.FAILED);
|
removeConditionBy(conditions, ConditionType.INITIALIZED);
|
||||||
|
removeConditionBy(conditions, ConditionType.READY);
|
||||||
|
conditions.addAndEvictFIFO(condition);
|
||||||
|
status.setPhase(Plugin.Phase.UNKNOWN);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,6 +221,14 @@ public class PluginReconciler implements Reconciler<Request> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String requestToUnload(Plugin plugin) {
|
||||||
|
var labels = plugin.getMetadata().getLabels();
|
||||||
|
if (labels == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return labels.get(REQUEST_TO_UNLOAD_LABEL);
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean requestToReload(Plugin plugin) {
|
private static boolean requestToReload(Plugin plugin) {
|
||||||
var annotations = plugin.getMetadata().getAnnotations();
|
var annotations = plugin.getMetadata().getAnnotations();
|
||||||
return annotations != null && annotations.get(RELOAD_ANNO) != null;
|
return annotations != null && annotations.get(RELOAD_ANNO) != null;
|
||||||
|
|
@ -209,7 +241,6 @@ public class PluginReconciler implements Reconciler<Request> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void cleanupResources(Plugin plugin) {
|
private void cleanupResources(Plugin plugin) {
|
||||||
var pluginName = plugin.getMetadata().getName();
|
var pluginName = plugin.getMetadata().getName();
|
||||||
var reverseProxyName = buildReverseProxyName(pluginName);
|
var reverseProxyName = buildReverseProxyName(pluginName);
|
||||||
|
|
@ -241,90 +272,121 @@ public class PluginReconciler implements Reconciler<Request> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void enablePlugin(Plugin plugin) {
|
private Result enablePlugin(Plugin plugin) {
|
||||||
// start the plugin
|
// start the plugin
|
||||||
var pluginName = plugin.getMetadata().getName();
|
var pluginName = plugin.getMetadata().getName();
|
||||||
log.info("Starting plugin {}", pluginName);
|
log.info("Starting plugin {}", pluginName);
|
||||||
plugin.statusNonNull().setPhase(Plugin.Phase.STARTING);
|
var status = plugin.getStatus();
|
||||||
|
status.setPhase(Plugin.Phase.STARTING);
|
||||||
|
|
||||||
|
// check if the parent plugin is started
|
||||||
|
var pw = pluginManager.getPlugin(pluginName);
|
||||||
|
var unstartedDependencies = pw.getDescriptor().getDependencies()
|
||||||
|
.stream()
|
||||||
|
.filter(pd -> {
|
||||||
|
if (pd.isOptional()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var parent = pluginManager.getPlugin(pd.getPluginId());
|
||||||
|
return parent == null || !PluginState.STARTED.equals(parent.getPluginState());
|
||||||
|
})
|
||||||
|
.map(PluginDependency::getPluginId)
|
||||||
|
.toList();
|
||||||
|
var conditions = status.getConditions();
|
||||||
|
if (!CollectionUtils.isEmpty(unstartedDependencies)) {
|
||||||
|
removeConditionBy(conditions, ConditionType.READY);
|
||||||
|
conditions.addAndEvictFIFO(Condition.builder()
|
||||||
|
.type(ConditionType.PROGRESSING)
|
||||||
|
.status(ConditionStatus.UNKNOWN)
|
||||||
|
.reason(ConditionReason.WAIT_FOR_DEPENDENCIES_STARTED)
|
||||||
|
.message("Wait for parent plugins " + unstartedDependencies + " to be started")
|
||||||
|
.lastTransitionTime(clock.instant())
|
||||||
|
.build());
|
||||||
|
status.setPhase(Plugin.Phase.UNKNOWN);
|
||||||
|
return Result.requeue(Duration.ofSeconds(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
var pluginState = pluginManager.startPlugin(pluginName);
|
var pluginState = pluginManager.startPlugin(pluginName);
|
||||||
if (!PluginState.STARTED.equals(pluginState)) {
|
if (!PluginState.STARTED.equals(pluginState)) {
|
||||||
throw new IllegalStateException("Failed to start plugin " + pluginName);
|
throw new IllegalStateException("""
|
||||||
|
Failed to start plugin %s(%s).\
|
||||||
|
""".formatted(pluginName, pluginState));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
conditions.addAndEvictFIFO(Condition.builder()
|
||||||
|
.type(ConditionType.READY)
|
||||||
|
.status(ConditionStatus.FALSE)
|
||||||
|
.reason(ConditionReason.START_ERROR)
|
||||||
|
.message(e.getMessage())
|
||||||
|
.lastTransitionTime(clock.instant())
|
||||||
|
.build());
|
||||||
|
status.setPhase(Plugin.Phase.FAILED);
|
||||||
|
return Result.doNotRetry();
|
||||||
}
|
}
|
||||||
|
|
||||||
var dependents = getAndRemoveDependents(plugin);
|
removeConditionBy(conditions, ConditionType.PROGRESSING);
|
||||||
log.info("Starting dependents {} for plugin {}", dependents, pluginName);
|
status.setLastStartTime(clock.instant());
|
||||||
dependents.stream()
|
conditions.addAndEvictFIFO(Condition.builder()
|
||||||
.sorted(Comparator.reverseOrder())
|
.type(ConditionType.READY)
|
||||||
.forEach(dependent -> {
|
.status(ConditionStatus.TRUE)
|
||||||
if (pluginManager.getPlugin(dependent) != null) {
|
.reason(ConditionReason.STARTED)
|
||||||
pluginManager.startPlugin(dependent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
log.info("Started dependents {} for plugin {}", dependents, pluginName);
|
|
||||||
|
|
||||||
plugin.statusNonNull().setLastStartTime(clock.instant());
|
|
||||||
var condition = Condition.builder()
|
|
||||||
.type(PluginState.STARTED.toString())
|
|
||||||
.reason(PluginState.STARTED.toString())
|
|
||||||
.message("Started successfully")
|
.message("Started successfully")
|
||||||
.lastTransitionTime(clock.instant())
|
.lastTransitionTime(clock.instant())
|
||||||
.status(ConditionStatus.TRUE)
|
.build());
|
||||||
.build();
|
status.setPhase(Plugin.Phase.STARTED);
|
||||||
nullSafeConditions(plugin.statusNonNull()).addAndEvictFIFO(condition);
|
|
||||||
plugin.statusNonNull().setPhase(Plugin.Phase.STARTED);
|
|
||||||
|
|
||||||
log.info("Started plugin {}", pluginName);
|
log.info("Started plugin {}", pluginName);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> getAndRemoveDependents(Plugin plugin) {
|
private Result disablePlugin(Plugin plugin) {
|
||||||
var pluginName = plugin.getMetadata().getName();
|
var pluginName = plugin.getMetadata().getName();
|
||||||
var annotations = plugin.getMetadata().getAnnotations();
|
var status = plugin.getStatus();
|
||||||
if (annotations == null) {
|
if (pluginManager.getPlugin(pluginName) != null) {
|
||||||
return List.of();
|
// check if the plugin has children
|
||||||
}
|
|
||||||
var dependentsAnno = annotations.remove(DEPENDENTS_ANNO_KEY);
|
|
||||||
List<String> dependents = List.of();
|
|
||||||
if (StringUtils.isNotBlank(dependentsAnno)) {
|
|
||||||
try {
|
|
||||||
dependents = JsonUtils.jsonToObject(dependentsAnno, new TypeReference<>() {
|
|
||||||
});
|
|
||||||
} catch (JsonParseException ignored) {
|
|
||||||
log.error("Failed to parse dependents annotation {} for plugin {}",
|
|
||||||
dependentsAnno, pluginName);
|
|
||||||
// Keep going to start the plugin.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dependents;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setDependents(Plugin plugin) {
|
|
||||||
var pluginName = plugin.getMetadata().getName();
|
|
||||||
var annotations = plugin.getMetadata().getAnnotations();
|
|
||||||
if (annotations == null) {
|
|
||||||
annotations = new HashMap<>();
|
|
||||||
plugin.getMetadata().setAnnotations(annotations);
|
|
||||||
}
|
|
||||||
if (!annotations.containsKey(DEPENDENTS_ANNO_KEY)) {
|
|
||||||
// get dependents
|
|
||||||
var dependents = pluginManager.getDependents(pluginName)
|
var dependents = pluginManager.getDependents(pluginName)
|
||||||
.stream()
|
.stream()
|
||||||
.filter(pluginWrapper ->
|
.filter(pw -> PluginState.STARTED.equals(pw.getPluginState()))
|
||||||
Objects.equals(PluginState.STARTED, pluginWrapper.getPluginState())
|
|
||||||
)
|
|
||||||
.map(PluginWrapper::getPluginId)
|
.map(PluginWrapper::getPluginId)
|
||||||
.toList();
|
.toList();
|
||||||
|
var conditions = status.getConditions();
|
||||||
annotations.put(DEPENDENTS_ANNO_KEY, JsonUtils.objectToJson(dependents));
|
if (!CollectionUtils.isEmpty(dependents)) {
|
||||||
|
removeConditionBy(conditions, ConditionType.READY);
|
||||||
|
conditions.addAndEvictFIFO(Condition.builder()
|
||||||
|
.type(ConditionType.PROGRESSING)
|
||||||
|
.status(ConditionStatus.UNKNOWN)
|
||||||
|
.reason(ConditionReason.WAIT_FOR_DEPENDENTS_DISABLED)
|
||||||
|
.message("Wait for children plugins " + dependents + " to be disabled")
|
||||||
|
.lastTransitionTime(clock.instant())
|
||||||
|
.build());
|
||||||
|
status.setPhase(Plugin.Phase.DISABLING);
|
||||||
|
return Result.requeue(Duration.ofSeconds(1));
|
||||||
}
|
}
|
||||||
}
|
try {
|
||||||
|
|
||||||
private void disablePlugin(Plugin plugin) {
|
|
||||||
var pluginName = plugin.getMetadata().getName();
|
|
||||||
if (pluginManager.getPlugin(pluginName) != null) {
|
|
||||||
setDependents(plugin);
|
|
||||||
pluginManager.disablePlugin(pluginName);
|
pluginManager.disablePlugin(pluginName);
|
||||||
|
} catch (Exception e) {
|
||||||
|
conditions.addAndEvictFIFO(Condition.builder()
|
||||||
|
.type(ConditionType.READY)
|
||||||
|
.status(ConditionStatus.FALSE)
|
||||||
|
.reason(ConditionReason.DISABLE_ERROR)
|
||||||
|
.message(e.getMessage())
|
||||||
|
.lastTransitionTime(clock.instant())
|
||||||
|
.build());
|
||||||
|
status.setPhase(Plugin.Phase.FAILED);
|
||||||
|
return Result.doNotRetry();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
var conditions = plugin.getStatus().getConditions();
|
||||||
|
removeConditionBy(conditions, ConditionType.PROGRESSING);
|
||||||
|
conditions.addAndEvictFIFO(Condition.builder()
|
||||||
|
.type(ConditionType.READY)
|
||||||
|
.status(ConditionStatus.TRUE)
|
||||||
|
.reason(ConditionReason.DISABLED)
|
||||||
|
.lastTransitionTime(clock.instant())
|
||||||
|
.build());
|
||||||
plugin.statusNonNull().setPhase(Plugin.Phase.DISABLED);
|
plugin.statusNonNull().setPhase(Plugin.Phase.DISABLED);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean requestToEnable(Plugin plugin) {
|
private static boolean requestToEnable(Plugin plugin) {
|
||||||
|
|
@ -332,7 +394,7 @@ public class PluginReconciler implements Reconciler<Request> {
|
||||||
return enabled != null && enabled;
|
return enabled != null && enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resolveStaticResources(Plugin plugin) {
|
private Result resolveStaticResources(Plugin plugin) {
|
||||||
var pluginName = plugin.getMetadata().getName();
|
var pluginName = plugin.getMetadata().getName();
|
||||||
var pluginVersion = plugin.getSpec().getVersion();
|
var pluginVersion = plugin.getSpec().getVersion();
|
||||||
if (isDevelopmentMode(plugin)) {
|
if (isDevelopmentMode(plugin)) {
|
||||||
|
|
@ -386,44 +448,121 @@ public class PluginReconciler implements Reconciler<Request> {
|
||||||
.toString();
|
.toString();
|
||||||
status.setStylesheet(stylesheet);
|
status.setStylesheet(stylesheet);
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadOrReload(Plugin plugin) {
|
private Result loadOrReload(Plugin plugin) {
|
||||||
var pluginName = plugin.getMetadata().getName();
|
var pluginName = plugin.getMetadata().getName();
|
||||||
var p = pluginManager.getPlugin(pluginName);
|
var p = pluginManager.getPlugin(pluginName);
|
||||||
|
var conditions = plugin.getStatus().getConditions();
|
||||||
|
|
||||||
|
var requestToUnloadBy = requestToUnload(plugin);
|
||||||
|
var requestToUnload = requestToUnloadBy != null;
|
||||||
|
var notFullyLoaded = p != null && pluginManager.getUnresolvedPlugins().contains(p);
|
||||||
|
var alreadyLoaded = p != null && pluginManager.getResolvedPlugins().contains(p);
|
||||||
|
|
||||||
var requestToReload = requestToReload(plugin);
|
var requestToReload = requestToReload(plugin);
|
||||||
if (requestToReload) {
|
// TODO Check load location
|
||||||
if (p != null) {
|
var shouldUnload = requestToUnload || requestToReload || notFullyLoaded;
|
||||||
var loadLocation = plugin.getStatus().getLoadLocation();
|
if (shouldUnload) {
|
||||||
setDependents(plugin);
|
// check if the plugin is already loaded or not fully loaded.
|
||||||
var unloaded = pluginManager.reloadPlugin(pluginName, Paths.get(loadLocation));
|
if (alreadyLoaded || notFullyLoaded) {
|
||||||
if (!unloaded) {
|
// get all dependencies
|
||||||
throw new PluginRuntimeException("Failed to reload plugin " + pluginName);
|
var dependents = requestToUnloadChildren(pluginName);
|
||||||
|
if (!CollectionUtils.isEmpty(dependents)) {
|
||||||
|
removeConditionBy(conditions, ConditionType.READY);
|
||||||
|
conditions.addAndEvictFIFO(Condition.builder()
|
||||||
|
.type(ConditionType.PROGRESSING)
|
||||||
|
.status(ConditionStatus.UNKNOWN)
|
||||||
|
.reason(ConditionReason.WAIT_FOR_DEPENDENTS_UNLOADED)
|
||||||
|
.message("Wait for children plugins " + dependents + "to be unloaded")
|
||||||
|
.lastTransitionTime(clock.instant())
|
||||||
|
.build());
|
||||||
|
plugin.getStatus().setPhase(Plugin.Phase.UNKNOWN);
|
||||||
|
// wait for children plugins unloaded
|
||||||
|
// retry after 1 second
|
||||||
|
return Result.requeue(Duration.ofSeconds(1));
|
||||||
}
|
}
|
||||||
p = pluginManager.getPlugin(pluginName);
|
|
||||||
}
|
// unload the plugin exactly
|
||||||
// ensure removing the reload annotation after the plugin is reloaded
|
|
||||||
removeRequestToReload(plugin);
|
|
||||||
}
|
|
||||||
if (p != null && pluginManager.getUnresolvedPlugins().contains(p)) {
|
|
||||||
pluginManager.unloadPlugin(pluginName);
|
pluginManager.unloadPlugin(pluginName);
|
||||||
|
|
||||||
|
removeConditionBy(conditions, ConditionType.INITIALIZED);
|
||||||
|
removeConditionBy(conditions, ConditionType.PROGRESSING);
|
||||||
|
removeConditionBy(conditions, ConditionType.READY);
|
||||||
|
|
||||||
|
cancelUnloadRequest(pluginName);
|
||||||
p = null;
|
p = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensure removing the reload annotation after the plugin is unloaded
|
||||||
|
if (requestToUnload) {
|
||||||
|
// skip loading and wait for removing the annotation by other plugins.
|
||||||
|
var status = plugin.getStatus();
|
||||||
|
status.getConditions().addAndEvictFIFO(Condition.builder()
|
||||||
|
.type(ConditionType.INITIALIZED)
|
||||||
|
.status(ConditionStatus.FALSE)
|
||||||
|
.reason(ConditionReason.REQUEST_TO_UNLOAD)
|
||||||
|
.message("Request to unload by " + requestToUnloadBy)
|
||||||
|
.lastTransitionTime(clock.instant())
|
||||||
|
.build());
|
||||||
|
return Result.doNotRetry();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestToReload) {
|
||||||
|
removeRequestToReload(plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check dependencies before loading
|
||||||
|
var unresolvedParentPlugins = plugin.getSpec().getPluginDependencies().keySet()
|
||||||
|
.stream()
|
||||||
|
.filter(dependency -> {
|
||||||
|
var parentPlugin = pluginManager.getPlugin(dependency);
|
||||||
|
return parentPlugin == null
|
||||||
|
|| pluginManager.getUnresolvedPlugins().contains(parentPlugin);
|
||||||
|
})
|
||||||
|
.sorted()
|
||||||
|
.toList();
|
||||||
|
if (!unresolvedParentPlugins.isEmpty()) {
|
||||||
|
// requeue if the parent plugin is not loaded yet.
|
||||||
|
removeConditionBy(conditions, ConditionType.INITIALIZED);
|
||||||
|
removeConditionBy(conditions, ConditionType.READY);
|
||||||
|
conditions.addAndEvictFIFO(Condition.builder()
|
||||||
|
.type(ConditionType.PROGRESSING)
|
||||||
|
.status(ConditionStatus.UNKNOWN)
|
||||||
|
.reason(ConditionReason.WAIT_FOR_DEPENDENCIES_LOADED)
|
||||||
|
.message("Wait for parent plugins " + unresolvedParentPlugins + " to be loaded")
|
||||||
|
.lastTransitionTime(clock.instant())
|
||||||
|
.build());
|
||||||
|
plugin.getStatus().setPhase(Plugin.Phase.UNKNOWN);
|
||||||
|
return Result.requeue(Duration.ofSeconds(1));
|
||||||
|
}
|
||||||
|
|
||||||
if (p == null) {
|
if (p == null) {
|
||||||
var loadLocation = plugin.getStatus().getLoadLocation();
|
var loadLocation = plugin.getStatus().getLoadLocation();
|
||||||
log.info("Loading plugin {} from {}", pluginName, loadLocation);
|
log.info("Loading plugin {} from {}", pluginName, loadLocation);
|
||||||
pluginManager.loadPlugin(Paths.get(loadLocation));
|
pluginManager.loadPlugin(Paths.get(loadLocation));
|
||||||
log.info("Loaded plugin {} from {}", pluginName, loadLocation);
|
log.info("Loaded plugin {} from {}", pluginName, loadLocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
conditions.addAndEvictFIFO(Condition.builder()
|
||||||
|
.type(ConditionType.INITIALIZED)
|
||||||
|
.status(ConditionStatus.TRUE)
|
||||||
|
.reason(ConditionReason.LOADED)
|
||||||
|
.lastTransitionTime(clock.instant())
|
||||||
|
.build());
|
||||||
|
plugin.getStatus().setPhase(Plugin.Phase.RESOLVED);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createOrUpdateSetting(Plugin plugin) {
|
private Result createOrUpdateSetting(Plugin plugin) {
|
||||||
log.info("Initializing setting and config map for plugin {}",
|
log.info("Initializing setting and config map for plugin {}",
|
||||||
plugin.getMetadata().getName());
|
plugin.getMetadata().getName());
|
||||||
var settingName = plugin.getSpec().getSettingName();
|
var settingName = plugin.getSpec().getSettingName();
|
||||||
if (StringUtils.isBlank(settingName)) {
|
if (StringUtils.isBlank(settingName)) {
|
||||||
// do nothing if no setting name provided.
|
// do nothing if no setting name provided.
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var pluginName = plugin.getMetadata().getName();
|
var pluginName = plugin.getMetadata().getName();
|
||||||
|
|
@ -454,7 +593,7 @@ public class PluginReconciler implements Reconciler<Request> {
|
||||||
// create default config map
|
// create default config map
|
||||||
var configMapName = plugin.getSpec().getConfigMapName();
|
var configMapName = plugin.getSpec().getConfigMapName();
|
||||||
if (StringUtils.isBlank(configMapName)) {
|
if (StringUtils.isBlank(configMapName)) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultConfigMap = SettingUtils.populateDefaultConfig(setting, configMapName);
|
var defaultConfigMap = SettingUtils.populateDefaultConfig(setting, configMapName);
|
||||||
|
|
@ -469,9 +608,10 @@ public class PluginReconciler implements Reconciler<Request> {
|
||||||
client.update(configMap);
|
client.update(configMap);
|
||||||
}, () -> client.create(defaultConfigMap));
|
}, () -> client.create(defaultConfigMap));
|
||||||
log.info("Initialized config map {} for plugin {}", configMapName, pluginName);
|
log.info("Initialized config map {} for plugin {}", configMapName, pluginName);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resolveLoadLocation(Plugin plugin) {
|
private Result resolveLoadLocation(Plugin plugin) {
|
||||||
log.debug("Resolving load location for plugin {}", plugin.getMetadata().getName());
|
log.debug("Resolving load location for plugin {}", plugin.getMetadata().getName());
|
||||||
|
|
||||||
// populate load location from annotations
|
// populate load location from annotations
|
||||||
|
|
@ -481,24 +621,46 @@ public class PluginReconciler implements Reconciler<Request> {
|
||||||
var status = plugin.statusNonNull();
|
var status = plugin.statusNonNull();
|
||||||
if (isDevelopmentMode(plugin)) {
|
if (isDevelopmentMode(plugin)) {
|
||||||
if (!isInDevEnvironment()) {
|
if (!isInDevEnvironment()) {
|
||||||
throw new IllegalStateException(String.format("""
|
status.getConditions().addAndEvictFIFO(Condition.builder()
|
||||||
Cannot run the plugin %s with dev mode in non-development environment.\
|
.type(ConditionType.INITIALIZED)
|
||||||
""", pluginName));
|
.status(ConditionStatus.FALSE)
|
||||||
|
.reason(ConditionReason.INVALID_RUNTIME_MODE)
|
||||||
|
.message("""
|
||||||
|
Cannot run the plugin with development mode in non-development environment.\
|
||||||
|
""")
|
||||||
|
.lastTransitionTime(clock.instant())
|
||||||
|
.build());
|
||||||
|
status.setPhase(Plugin.Phase.UNKNOWN);
|
||||||
|
return Result.doNotRetry();
|
||||||
}
|
}
|
||||||
log.debug("Plugin {} is in development mode", pluginName);
|
log.debug("Plugin {} is in development mode", pluginName);
|
||||||
if (StringUtils.isBlank(pluginPathAnno)) {
|
if (StringUtils.isBlank(pluginPathAnno)) {
|
||||||
// should never happen.
|
status.getConditions().addAndEvictFIFO(Condition.builder()
|
||||||
throw new IllegalArgumentException(String.format("""
|
.type(ConditionType.INITIALIZED)
|
||||||
Please set plugin path annotation "%s" \
|
.status(ConditionStatus.FALSE)
|
||||||
in development mode for plugin %s.""",
|
.reason(ConditionReason.PLUGIN_PATH_NOT_SET)
|
||||||
RUNTIME_MODE_ANNO, pluginName));
|
.message("""
|
||||||
|
Plugin path annotation is not set. \
|
||||||
|
Please set plugin path annotation "%s" in development mode.\
|
||||||
|
""".formatted(PLUGIN_PATH))
|
||||||
|
.build());
|
||||||
|
return Result.doNotRetry();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
var loadLocation = ResourceUtils.getURL(pluginPathAnno).toURI();
|
var loadLocation = ResourceUtils.getURL(pluginPathAnno).toURI();
|
||||||
status.setLoadLocation(loadLocation);
|
status.setLoadLocation(loadLocation);
|
||||||
} catch (URISyntaxException | FileNotFoundException e) {
|
} catch (URISyntaxException | FileNotFoundException e) {
|
||||||
throw new IllegalArgumentException(
|
// TODO Refactor this using event in the future.
|
||||||
"Invalid plugin path " + pluginPathAnno + " configured.", e);
|
var condition = Condition.builder()
|
||||||
|
.type(ConditionType.INITIALIZED)
|
||||||
|
.status(ConditionStatus.FALSE)
|
||||||
|
.reason(ConditionReason.INVALID_PLUGIN_PATH)
|
||||||
|
.message("Invalid plugin path " + pluginPathAnno + " configured.")
|
||||||
|
.lastTransitionTime(clock.instant())
|
||||||
|
.build();
|
||||||
|
status.getConditions().addAndEvictFIFO(condition);
|
||||||
|
status.setPhase(Plugin.Phase.UNKNOWN);
|
||||||
|
return Result.doNotRetry();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// reset annotation PLUGIN_PATH in non-dev mode
|
// reset annotation PLUGIN_PATH in non-dev mode
|
||||||
|
|
@ -535,7 +697,16 @@ public class PluginReconciler implements Reconciler<Request> {
|
||||||
}
|
}
|
||||||
status.setLoadLocation(newLoadLocation);
|
status.setLoadLocation(newLoadLocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
status.getConditions().addAndEvictFIFO(Condition.builder()
|
||||||
|
.type(ConditionType.INITIALIZED)
|
||||||
|
.status(ConditionStatus.TRUE)
|
||||||
|
.reason(ConditionReason.LOAD_LOCATION_RESOLVED)
|
||||||
|
.lastTransitionTime(clock.instant())
|
||||||
|
.build());
|
||||||
|
status.setPhase(Plugin.Phase.RESOLVED);
|
||||||
log.debug("Populated load location {} for plugin {}", status.getLoadLocation(), pluginName);
|
log.debug("Populated load location {} for plugin {}", status.getLoadLocation(), pluginName);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -545,7 +716,7 @@ public class PluginReconciler implements Reconciler<Request> {
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
void createOrUpdateReverseProxy(Plugin plugin) {
|
private Result createOrUpdateReverseProxy(Plugin plugin) {
|
||||||
String pluginName = plugin.getMetadata().getName();
|
String pluginName = plugin.getMetadata().getName();
|
||||||
String reverseProxyName = buildReverseProxyName(pluginName);
|
String reverseProxyName = buildReverseProxyName(pluginName);
|
||||||
ReverseProxy reverseProxy = new ReverseProxy();
|
ReverseProxy reverseProxy = new ReverseProxy();
|
||||||
|
|
@ -570,6 +741,7 @@ public class PluginReconciler implements Reconciler<Request> {
|
||||||
.setVersion(persisted.getMetadata().getVersion());
|
.setVersion(persisted.getMetadata().getVersion());
|
||||||
client.update(reverseProxy);
|
client.update(reverseProxy);
|
||||||
}, () -> client.create(reverseProxy));
|
}, () -> client.create(reverseProxy));
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Path getPluginsRoot() {
|
private Path getPluginsRoot() {
|
||||||
|
|
@ -587,40 +759,88 @@ public class PluginReconciler implements Reconciler<Request> {
|
||||||
return pluginName + "-system-generated-reverse-proxy";
|
return pluginName + "-system-generated-reverse-proxy";
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PluginStartedListener implements PluginStateListener {
|
private List<String> requestToUnloadChildren(String pluginName) {
|
||||||
|
// get all dependencies
|
||||||
|
var dependents = pluginManager.getDependents(pluginName)
|
||||||
|
.stream()
|
||||||
|
.map(PluginWrapper::getPluginId)
|
||||||
|
.toList();
|
||||||
|
// request all dependents to reload.
|
||||||
|
dependents.forEach(dependent -> client.fetch(Plugin.class, dependent)
|
||||||
|
.ifPresent(childPlugin -> {
|
||||||
|
var labels = childPlugin.getMetadata().getLabels();
|
||||||
|
if (labels == null) {
|
||||||
|
labels = new HashMap<>();
|
||||||
|
childPlugin.getMetadata().setLabels(labels);
|
||||||
|
}
|
||||||
|
var label = labels.get(REQUEST_TO_UNLOAD_LABEL);
|
||||||
|
if (!pluginName.equals(label)) {
|
||||||
|
labels.put(REQUEST_TO_UNLOAD_LABEL, pluginName);
|
||||||
|
client.update(childPlugin);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
return dependents;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
private void cancelUnloadRequest(String pluginName) {
|
||||||
public void pluginStateChanged(PluginStateEvent event) {
|
// remove label REQUEST_TO_UNLOAD_LABEL
|
||||||
if (PluginState.STARTED.equals(event.getPluginState())) {
|
// TODO Use index mechanism
|
||||||
var pluginId = event.getPlugin().getPluginId();
|
Predicate<Plugin> filter = aplugin -> {
|
||||||
client.fetch(Plugin.class, pluginId)
|
var labels = aplugin.getMetadata().getLabels();
|
||||||
.ifPresent(plugin -> {
|
return labels != null && pluginName.equals(labels.get(REQUEST_TO_UNLOAD_LABEL));
|
||||||
if (!Objects.equals(true, plugin.getSpec().getEnabled())) {
|
};
|
||||||
log.info("Observed plugin {} started, enabling it.", pluginId);
|
|
||||||
plugin.getSpec().setEnabled(true);
|
client.list(Plugin.class, filter, null)
|
||||||
client.update(plugin);
|
.forEach(aplugin -> {
|
||||||
|
var labels = aplugin.getMetadata().getLabels();
|
||||||
|
if (labels != null && labels.remove(REQUEST_TO_UNLOAD_LABEL) != null) {
|
||||||
|
client.update(aplugin);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PluginStoppedListener implements PluginStateListener {
|
private static void removeConditionBy(ConditionList conditions, String type) {
|
||||||
|
conditions.removeIf(condition -> Objects.equals(type, condition.getType()));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
public static class ConditionType {
|
||||||
public void pluginStateChanged(PluginStateEvent event) {
|
/**
|
||||||
if (PluginState.STOPPED.equals(event.getPluginState())) {
|
* Indicates whether the plugin is initialized.
|
||||||
var pluginId = event.getPlugin().getPluginId();
|
*/
|
||||||
client.fetch(Plugin.class, pluginId)
|
public static final String INITIALIZED = "Initialized";
|
||||||
.ifPresent(plugin -> {
|
|
||||||
if (!requestToReload(plugin)
|
/**
|
||||||
&& Objects.equals(true, plugin.getSpec().getEnabled())) {
|
* Indicates whether the plugin is starting, disabling or deleting.
|
||||||
log.info("Observed plugin {} stopped, disabling it.", pluginId);
|
*/
|
||||||
plugin.getSpec().setEnabled(false);
|
public static final String PROGRESSING = "Progressing";
|
||||||
client.update(plugin);
|
|
||||||
}
|
/**
|
||||||
});
|
* Indicates whether the plugin is ready.
|
||||||
}
|
*/
|
||||||
|
public static final String READY = "Ready";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class ConditionReason {
|
||||||
|
public static final String LOAD_LOCATION_RESOLVED = "LoadLocationResolved";
|
||||||
|
public static final String INVALID_PLUGIN_PATH = "InvalidPluginPath";
|
||||||
|
|
||||||
|
public static final String WAIT_FOR_DEPENDENCIES_STARTED = "WaitForDependenciesStarted";
|
||||||
|
public static final String WAIT_FOR_DEPENDENCIES_LOADED = "WaitForDependenciesLoaded";
|
||||||
|
|
||||||
|
public static final String WAIT_FOR_DEPENDENTS_DELETED = "WaitForDependentsDeleted";
|
||||||
|
public static final String WAIT_FOR_DEPENDENTS_DISABLED = "WaitForDependentsDisabled";
|
||||||
|
public static final String WAIT_FOR_DEPENDENTS_UNLOADED = "WaitForDependentsUnloaded";
|
||||||
|
|
||||||
|
public static final String STARTED = "Started";
|
||||||
|
public static final String DISABLED = "Disabled";
|
||||||
|
public static final String SYSTEM_ERROR = "SystemError";
|
||||||
|
public static final String REQUEST_TO_UNLOAD = "RequestToUnload";
|
||||||
|
public static final String LOADED = "Loaded";
|
||||||
|
public static final String START_ERROR = "StartError";
|
||||||
|
public static final String DISABLE_ERROR = "DisableError";
|
||||||
|
public static final String INVALID_RUNTIME_MODE = "InvalidRuntimeMode";
|
||||||
|
public static final String PLUGIN_PATH_NOT_SET = "PluginPathNotSet";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,4 +62,15 @@ public interface PluginService {
|
||||||
* @return signed js bundle version by all enabled plugins version.
|
* @return signed js bundle version by all enabled plugins version.
|
||||||
*/
|
*/
|
||||||
Mono<String> generateJsBundleVersion();
|
Mono<String> generateJsBundleVersion();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables or disables a plugin by name.
|
||||||
|
*
|
||||||
|
* @param pluginName plugin name
|
||||||
|
* @param requestToEnable request to enable or disable
|
||||||
|
* @param wait wait for plugin to be enabled or disabled
|
||||||
|
* @return updated plugin
|
||||||
|
*/
|
||||||
|
Mono<Plugin> changeState(String pluginName, boolean requestToEnable, boolean wait);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package run.halo.app.core.extension.service.impl;
|
package run.halo.app.core.extension.service.impl;
|
||||||
|
|
||||||
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
|
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
|
||||||
|
import static org.pf4j.PluginState.STARTED;
|
||||||
import static run.halo.app.plugin.PluginConst.RELOAD_ANNO;
|
import static run.halo.app.plugin.PluginConst.RELOAD_ANNO;
|
||||||
|
|
||||||
import com.github.zafarkhaja.semver.Version;
|
import com.github.zafarkhaja.semver.Version;
|
||||||
|
|
@ -10,6 +11,7 @@ import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|
@ -20,7 +22,6 @@ import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.pf4j.DependencyResolver;
|
import org.pf4j.DependencyResolver;
|
||||||
import org.pf4j.PluginDescriptor;
|
import org.pf4j.PluginDescriptor;
|
||||||
import org.pf4j.PluginManager;
|
|
||||||
import org.pf4j.PluginWrapper;
|
import org.pf4j.PluginWrapper;
|
||||||
import org.pf4j.RuntimeMode;
|
import org.pf4j.RuntimeMode;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
|
|
@ -36,12 +37,15 @@ import reactor.core.Exceptions;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.core.scheduler.Schedulers;
|
import reactor.core.scheduler.Schedulers;
|
||||||
|
import reactor.util.retry.Retry;
|
||||||
import run.halo.app.core.extension.Plugin;
|
import run.halo.app.core.extension.Plugin;
|
||||||
import run.halo.app.core.extension.service.PluginService;
|
import run.halo.app.core.extension.service.PluginService;
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.infra.SystemVersionSupplier;
|
import run.halo.app.infra.SystemVersionSupplier;
|
||||||
import run.halo.app.infra.exception.PluginAlreadyExistsException;
|
import run.halo.app.infra.exception.PluginAlreadyExistsException;
|
||||||
|
import run.halo.app.infra.exception.PluginDependenciesNotEnabledException;
|
||||||
import run.halo.app.infra.exception.PluginDependencyException;
|
import run.halo.app.infra.exception.PluginDependencyException;
|
||||||
|
import run.halo.app.infra.exception.PluginDependentsNotDisabledException;
|
||||||
import run.halo.app.infra.exception.PluginInstallationException;
|
import run.halo.app.infra.exception.PluginInstallationException;
|
||||||
import run.halo.app.infra.exception.UnsatisfiedAttributeValueException;
|
import run.halo.app.infra.exception.UnsatisfiedAttributeValueException;
|
||||||
import run.halo.app.infra.utils.FileUtils;
|
import run.halo.app.infra.utils.FileUtils;
|
||||||
|
|
@ -49,6 +53,7 @@ import run.halo.app.infra.utils.VersionUtils;
|
||||||
import run.halo.app.plugin.PluginConst;
|
import run.halo.app.plugin.PluginConst;
|
||||||
import run.halo.app.plugin.PluginProperties;
|
import run.halo.app.plugin.PluginProperties;
|
||||||
import run.halo.app.plugin.PluginUtils;
|
import run.halo.app.plugin.PluginUtils;
|
||||||
|
import run.halo.app.plugin.SpringPluginManager;
|
||||||
import run.halo.app.plugin.YamlPluginDescriptorFinder;
|
import run.halo.app.plugin.YamlPluginDescriptorFinder;
|
||||||
import run.halo.app.plugin.YamlPluginFinder;
|
import run.halo.app.plugin.YamlPluginFinder;
|
||||||
import run.halo.app.plugin.resources.BundleResourceUtils;
|
import run.halo.app.plugin.resources.BundleResourceUtils;
|
||||||
|
|
@ -67,7 +72,7 @@ public class PluginServiceImpl implements PluginService {
|
||||||
|
|
||||||
private final PluginProperties pluginProperties;
|
private final PluginProperties pluginProperties;
|
||||||
|
|
||||||
private final PluginManager pluginManager;
|
private final SpringPluginManager pluginManager;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Flux<Plugin> getPresets() {
|
public Flux<Plugin> getPresets() {
|
||||||
|
|
@ -252,6 +257,86 @@ public class PluginServiceImpl implements PluginService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Plugin> changeState(String pluginName, boolean requestToEnable, boolean wait) {
|
||||||
|
var updatedPlugin = Mono.defer(() -> client.get(Plugin.class, pluginName))
|
||||||
|
.flatMap(plugin -> {
|
||||||
|
if (!Objects.equals(requestToEnable, plugin.getSpec().getEnabled())) {
|
||||||
|
// preflight check
|
||||||
|
if (requestToEnable) {
|
||||||
|
// make sure the dependencies are enabled
|
||||||
|
var dependencies = plugin.getSpec().getPluginDependencies().keySet();
|
||||||
|
var notStartedDependencies = dependencies.stream()
|
||||||
|
.filter(dependency -> {
|
||||||
|
var pluginWrapper = pluginManager.getPlugin(dependency);
|
||||||
|
return pluginWrapper == null
|
||||||
|
|| !Objects.equals(STARTED, pluginWrapper.getPluginState());
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
if (!CollectionUtils.isEmpty(notStartedDependencies)) {
|
||||||
|
return Mono.error(
|
||||||
|
new PluginDependenciesNotEnabledException(notStartedDependencies)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// make sure the dependents are disabled
|
||||||
|
var dependents = pluginManager.getDependents(pluginName);
|
||||||
|
var notDisabledDependents = dependents.stream()
|
||||||
|
.filter(
|
||||||
|
dependent -> Objects.equals(STARTED, dependent.getPluginState())
|
||||||
|
)
|
||||||
|
.map(PluginWrapper::getPluginId)
|
||||||
|
.toList();
|
||||||
|
if (!CollectionUtils.isEmpty(notDisabledDependents)) {
|
||||||
|
return Mono.error(
|
||||||
|
new PluginDependentsNotDisabledException(notDisabledDependents)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin.getSpec().setEnabled(requestToEnable);
|
||||||
|
log.debug("Updating plugin {} state to {}", pluginName, requestToEnable);
|
||||||
|
return client.update(plugin);
|
||||||
|
}
|
||||||
|
log.debug("Checking plugin {} state, no need to update", pluginName);
|
||||||
|
return Mono.just(plugin);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (wait) {
|
||||||
|
// if we want to wait the state of plugin to be updated
|
||||||
|
updatedPlugin = updatedPlugin
|
||||||
|
.flatMap(plugin -> {
|
||||||
|
var phase = plugin.statusNonNull().getPhase();
|
||||||
|
if (requestToEnable) {
|
||||||
|
// if we request to enable the plugin
|
||||||
|
if (!(Plugin.Phase.STARTED.equals(phase)
|
||||||
|
|| Plugin.Phase.FAILED.equals(phase))) {
|
||||||
|
return Mono.error(UnexpectedPluginStateException::new);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if we request to disable the plugin
|
||||||
|
if (Plugin.Phase.STARTED.equals(phase)) {
|
||||||
|
return Mono.error(UnexpectedPluginStateException::new);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Mono.just(plugin);
|
||||||
|
})
|
||||||
|
.retryWhen(
|
||||||
|
Retry.backoff(10, Duration.ofMillis(100))
|
||||||
|
.filter(UnexpectedPluginStateException.class::isInstance)
|
||||||
|
.doBeforeRetry(signal ->
|
||||||
|
log.debug("Waiting for plugin {} to meet expected state", pluginName)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.doOnSuccess(plugin -> {
|
||||||
|
log.info("Plugin {} met expected state {}",
|
||||||
|
pluginName, plugin.statusNonNull().getPhase());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedPlugin;
|
||||||
|
}
|
||||||
|
|
||||||
Mono<Plugin> findPluginManifest(Path path) {
|
Mono<Plugin> findPluginManifest(Path path) {
|
||||||
return Mono.fromSupplier(
|
return Mono.fromSupplier(
|
||||||
() -> {
|
() -> {
|
||||||
|
|
@ -368,4 +453,9 @@ public class PluginServiceImpl implements PluginService {
|
||||||
oldPlugin.setSpec(newPlugin.getSpec());
|
oldPlugin.setSpec(newPlugin.getSpec());
|
||||||
oldPlugin.getSpec().setEnabled(enabled);
|
oldPlugin.getSpec().setEnabled(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class UnexpectedPluginStateException extends RuntimeException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package run.halo.app.infra.exception;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin dependencies not enabled exception.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
public class PluginDependenciesNotEnabledException extends ServerWebInputException {
|
||||||
|
|
||||||
|
public static final URI TYPE =
|
||||||
|
URI.create("https://www.halo.run/probs/plugin-dependencies-not-enabled");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new Plugin dependencies not enabled exception.
|
||||||
|
*
|
||||||
|
* @param dependencies dependencies that are not enabled
|
||||||
|
*/
|
||||||
|
public PluginDependenciesNotEnabledException(List<String> dependencies) {
|
||||||
|
super("Plugin dependencies are not fully enabled, please enable them first.",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new Object[] {dependencies});
|
||||||
|
setType(TYPE);
|
||||||
|
getBody().setProperty("dependencies", dependencies);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package run.halo.app.infra.exception;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin dependents not disabled exception.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
public class PluginDependentsNotDisabledException extends ServerWebInputException {
|
||||||
|
|
||||||
|
public static final URI TYPE =
|
||||||
|
URI.create("https://www.halo.run/probs/plugin-dependents-not-disabled");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new Plugin dependents not disabled exception.
|
||||||
|
*
|
||||||
|
* @param dependents dependents that are not disabled
|
||||||
|
*/
|
||||||
|
public PluginDependentsNotDisabledException(List<String> dependents) {
|
||||||
|
super("Plugin dependents are not fully disabled, please disable them first.",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new Object[] {dependents});
|
||||||
|
setType(TYPE);
|
||||||
|
getBody().setProperty("dependents", dependents);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -3,9 +3,7 @@ package run.halo.app.plugin;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Stack;
|
import java.util.Stack;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
@ -22,7 +20,6 @@ import org.pf4j.PluginDescriptorFinder;
|
||||||
import org.pf4j.PluginFactory;
|
import org.pf4j.PluginFactory;
|
||||||
import org.pf4j.PluginLoader;
|
import org.pf4j.PluginLoader;
|
||||||
import org.pf4j.PluginRepository;
|
import org.pf4j.PluginRepository;
|
||||||
import org.pf4j.PluginRuntimeException;
|
|
||||||
import org.pf4j.PluginStatusProvider;
|
import org.pf4j.PluginStatusProvider;
|
||||||
import org.pf4j.PluginWrapper;
|
import org.pf4j.PluginWrapper;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
|
|
@ -162,37 +159,6 @@ public class HaloPluginManager extends DefaultPluginManager implements SpringPlu
|
||||||
return sharedContext.get();
|
return sharedContext.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean reloadPlugin(String pluginId, Path loadLocation) {
|
|
||||||
log.info("Reloading plugin {} from {}", pluginId, loadLocation);
|
|
||||||
|
|
||||||
var dependents = getDependents(pluginId);
|
|
||||||
dependents.forEach(plugin -> unloadPlugin(plugin.getPluginId(), false));
|
|
||||||
|
|
||||||
var unloaded = unloadPlugin(pluginId, false);
|
|
||||||
if (!unloaded) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// load the root plugin
|
|
||||||
var loadedPluginId = loadPlugin(loadLocation);
|
|
||||||
|
|
||||||
if (!Objects.equals(pluginId, loadedPluginId)) {
|
|
||||||
throw new PluginRuntimeException("""
|
|
||||||
The plugin {} is reloaded successfully, \
|
|
||||||
but the plugin id is different from the original one.
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
// load all dependents with reverse order
|
|
||||||
dependents.stream()
|
|
||||||
.map(PluginWrapper::getPluginPath)
|
|
||||||
.sorted(Comparator.reverseOrder())
|
|
||||||
.forEach(this::loadPlugin);
|
|
||||||
|
|
||||||
log.info("Reloaded plugin {} from {}", loadedPluginId, loadLocation);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<PluginWrapper> getDependents(String pluginId) {
|
public List<PluginWrapper> getDependents(String pluginId) {
|
||||||
var dependents = new ArrayList<PluginWrapper>();
|
var dependents = new ArrayList<PluginWrapper>();
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ public interface PluginConst {
|
||||||
|
|
||||||
String RELOAD_ANNO = "plugin.halo.run/reload";
|
String RELOAD_ANNO = "plugin.halo.run/reload";
|
||||||
|
|
||||||
|
String REQUEST_TO_UNLOAD_LABEL = "plugin.halo.run/request-to-unload";
|
||||||
|
|
||||||
String PLUGIN_PATH = "plugin.halo.run/plugin-path";
|
String PLUGIN_PATH = "plugin.halo.run/plugin-path";
|
||||||
|
|
||||||
String RUNTIME_MODE_ANNO = "plugin.halo.run/runtime-mode";
|
String RUNTIME_MODE_ANNO = "plugin.halo.run/runtime-mode";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package run.halo.app.plugin;
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.pf4j.PluginManager;
|
import org.pf4j.PluginManager;
|
||||||
import org.pf4j.PluginWrapper;
|
import org.pf4j.PluginWrapper;
|
||||||
|
|
@ -12,15 +11,6 @@ public interface SpringPluginManager extends PluginManager {
|
||||||
|
|
||||||
ApplicationContext getSharedContext();
|
ApplicationContext getSharedContext();
|
||||||
|
|
||||||
/**
|
|
||||||
* Reload the plugin and the plugins that depend on it.
|
|
||||||
*
|
|
||||||
* @param pluginId plugin id
|
|
||||||
* @param loadLocation new load location
|
|
||||||
* @return true if reload successfully, otherwise false
|
|
||||||
*/
|
|
||||||
boolean reloadPlugin(String pluginId, Path loadLocation);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all dependents recursively.
|
* Get all dependents recursively.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ problemDetail.title.run.halo.app.infra.exception.EmailVerificationFailed=Email V
|
||||||
problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$CyclicException=Cyclic Dependency Detected
|
problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$CyclicException=Cyclic Dependency Detected
|
||||||
problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$NotFoundException=Dependencies Not Found
|
problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$NotFoundException=Dependencies Not Found
|
||||||
problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=Wrong Dependency Version
|
problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=Wrong Dependency Version
|
||||||
|
problemDetail.title.run.halo.app.infra.exception.PluginDependentsNotDisabledException=Dependents Not Disabled
|
||||||
|
problemDetail.title.run.halo.app.infra.exception.PluginDependenciesNotEnabledException=Dependencies Not Enabled
|
||||||
problemDetail.title.internalServerError=Internal Server Error
|
problemDetail.title.internalServerError=Internal Server Error
|
||||||
|
|
||||||
# Detail definitions
|
# Detail definitions
|
||||||
|
|
@ -46,6 +48,8 @@ problemDetail.run.halo.app.infra.exception.EmailVerificationFailed=Invalid email
|
||||||
problemDetail.run.halo.app.infra.exception.PluginDependencyException$CyclicException=A cyclic dependency was detected.
|
problemDetail.run.halo.app.infra.exception.PluginDependencyException$CyclicException=A cyclic dependency was detected.
|
||||||
problemDetail.run.halo.app.infra.exception.PluginDependencyException$NotFoundException=Dependencies "{0}" were not found.
|
problemDetail.run.halo.app.infra.exception.PluginDependencyException$NotFoundException=Dependencies "{0}" were not found.
|
||||||
problemDetail.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=Dependencies have wrong version: {0}.
|
problemDetail.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=Dependencies have wrong version: {0}.
|
||||||
|
problemDetail.run.halo.app.infra.exception.PluginDependentsNotDisabledException=Plugin dependents {0} are not fully disabled, please disable them first.
|
||||||
|
problemDetail.run.halo.app.infra.exception.PluginDependenciesNotEnabledException=Plugin dependencies {0} are not fully enabled, please enable them first.
|
||||||
|
|
||||||
problemDetail.index.duplicateKey=The value of {0} already exists for unique index {1}, please rename it and retry.
|
problemDetail.index.duplicateKey=The value of {0} already exists for unique index {1}, please rename it and retry.
|
||||||
problemDetail.user.email.verify.maxAttempts=Too many verification attempts, please try again later.
|
problemDetail.user.email.verify.maxAttempts=Too many verification attempts, please try again later.
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ problemDetail.title.run.halo.app.infra.exception.EmailVerificationFailed=邮箱
|
||||||
problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$CyclicException=循环依赖
|
problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$CyclicException=循环依赖
|
||||||
problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$NotFoundException=依赖未找到
|
problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$NotFoundException=依赖未找到
|
||||||
problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=依赖版本错误
|
problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=依赖版本错误
|
||||||
|
problemDetail.title.run.halo.app.infra.exception.PluginDependentsNotDisabledException=子插件未禁用
|
||||||
|
problemDetail.title.run.halo.app.infra.exception.PluginDependenciesNotEnabledException=依赖未启用
|
||||||
problemDetail.title.internalServerError=服务器内部错误
|
problemDetail.title.internalServerError=服务器内部错误
|
||||||
|
|
||||||
problemDetail.org.springframework.security.authentication.BadCredentialsException=用户名或密码错误。
|
problemDetail.org.springframework.security.authentication.BadCredentialsException=用户名或密码错误。
|
||||||
|
|
@ -23,6 +25,8 @@ problemDetail.run.halo.app.infra.exception.EmailVerificationFailed=验证码错
|
||||||
problemDetail.run.halo.app.infra.exception.PluginDependencyException$CyclicException=检测到循环依赖。
|
problemDetail.run.halo.app.infra.exception.PluginDependencyException$CyclicException=检测到循环依赖。
|
||||||
problemDetail.run.halo.app.infra.exception.PluginDependencyException$NotFoundException=依赖“{0}”未找到。
|
problemDetail.run.halo.app.infra.exception.PluginDependencyException$NotFoundException=依赖“{0}”未找到。
|
||||||
problemDetail.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=依赖版本有误:{0}。
|
problemDetail.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=依赖版本有误:{0}。
|
||||||
|
problemDetail.run.halo.app.infra.exception.PluginDependentsNotDisabledException=子插件 {0} 未完全禁用,请先禁用它们。
|
||||||
|
problemDetail.run.halo.app.infra.exception.PluginDependenciesNotEnabledException=插件依赖 {0} 未完全启用,请先启用它们。
|
||||||
|
|
||||||
problemDetail.index.duplicateKey=唯一索引 {1} 中的值 {0} 已存在,请更名后重试。
|
problemDetail.index.duplicateKey=唯一索引 {1} 中的值 {0} 已存在,请更名后重试。
|
||||||
problemDetail.user.email.verify.maxAttempts=尝试次数过多,请稍候再试。
|
problemDetail.user.email.verify.maxAttempts=尝试次数过多,请稍候再试。
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@ package run.halo.app.core.extension.endpoint;
|
||||||
|
|
||||||
import static java.util.Objects.requireNonNull;
|
import static java.util.Objects.requireNonNull;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
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.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.ArgumentMatchers.argThat;
|
import static org.mockito.ArgumentMatchers.argThat;
|
||||||
|
|
@ -11,7 +9,6 @@ import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.ArgumentMatchers.isA;
|
import static org.mockito.ArgumentMatchers.isA;
|
||||||
import static org.mockito.ArgumentMatchers.same;
|
import static org.mockito.ArgumentMatchers.same;
|
||||||
import static org.mockito.Mockito.lenient;
|
import static org.mockito.Mockito.lenient;
|
||||||
import static org.mockito.Mockito.times;
|
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
import static org.springframework.test.web.reactive.server.WebTestClient.bindToRouterFunction;
|
import static org.springframework.test.web.reactive.server.WebTestClient.bindToRouterFunction;
|
||||||
|
|
@ -478,69 +475,4 @@ class PluginEndpointTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
|
||||||
class PluginStateChangeTest {
|
|
||||||
|
|
||||||
WebTestClient webClient;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldEnablePluginIfPluginWasNotStarted() {
|
|
||||||
var plugin = createPlugin("fake-plugin");
|
|
||||||
plugin.getSpec().setEnabled(false);
|
|
||||||
plugin.statusNonNull().setPhase(Plugin.Phase.RESOLVED);
|
|
||||||
|
|
||||||
when(client.get(Plugin.class, "fake-plugin")).thenReturn(Mono.just(plugin))
|
|
||||||
.thenReturn(Mono.fromSupplier(() -> {
|
|
||||||
plugin.statusNonNull().setPhase(Plugin.Phase.STARTED);
|
|
||||||
return plugin;
|
|
||||||
}));
|
|
||||||
when(client.update(plugin)).thenReturn(Mono.just(plugin));
|
|
||||||
|
|
||||||
var requestBody = new PluginEndpoint.RunningStateRequest();
|
|
||||||
requestBody.setEnable(true);
|
|
||||||
requestBody.setAsync(false);
|
|
||||||
webClient.put().uri("/plugins/fake-plugin/plugin-state")
|
|
||||||
.bodyValue(requestBody)
|
|
||||||
.exchange()
|
|
||||||
.expectStatus().isOk()
|
|
||||||
.expectBody(Plugin.class)
|
|
||||||
.value(p -> assertTrue(p.getSpec().getEnabled()));
|
|
||||||
|
|
||||||
verify(client, times(2)).get(Plugin.class, "fake-plugin");
|
|
||||||
verify(client).update(plugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldDisablePluginIfAlreadyStarted() {
|
|
||||||
var plugin = createPlugin("fake-plugin");
|
|
||||||
plugin.getSpec().setEnabled(true);
|
|
||||||
plugin.statusNonNull().setPhase(Plugin.Phase.STARTED);
|
|
||||||
|
|
||||||
when(client.get(Plugin.class, "fake-plugin")).thenReturn(Mono.just(plugin))
|
|
||||||
.thenReturn(Mono.fromSupplier(() -> {
|
|
||||||
plugin.getStatus().setPhase(Plugin.Phase.STOPPED);
|
|
||||||
return plugin;
|
|
||||||
}));
|
|
||||||
when(client.update(plugin)).thenReturn(Mono.just(plugin));
|
|
||||||
|
|
||||||
var requestBody = new PluginEndpoint.RunningStateRequest();
|
|
||||||
requestBody.setEnable(false);
|
|
||||||
requestBody.setAsync(false);
|
|
||||||
webClient.put().uri("/plugins/fake-plugin/plugin-state")
|
|
||||||
.bodyValue(requestBody)
|
|
||||||
.exchange()
|
|
||||||
.expectStatus().isOk()
|
|
||||||
.expectBody(Plugin.class)
|
|
||||||
.value(p -> assertFalse(p.getSpec().getEnabled()));
|
|
||||||
|
|
||||||
verify(client, times(2)).get(Plugin.class, "fake-plugin");
|
|
||||||
verify(client).update(plugin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.Mockito.lenient;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
|
|
@ -40,6 +40,7 @@ import org.junit.jupiter.api.io.TempDir;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.pf4j.DefaultPluginDescriptor;
|
||||||
import org.pf4j.PluginState;
|
import org.pf4j.PluginState;
|
||||||
import org.pf4j.PluginWrapper;
|
import org.pf4j.PluginWrapper;
|
||||||
import org.pf4j.RuntimeMode;
|
import org.pf4j.RuntimeMode;
|
||||||
|
|
@ -53,6 +54,7 @@ import run.halo.app.extension.Metadata;
|
||||||
import run.halo.app.extension.controller.Reconciler;
|
import run.halo.app.extension.controller.Reconciler;
|
||||||
import run.halo.app.extension.controller.Reconciler.Request;
|
import run.halo.app.extension.controller.Reconciler.Request;
|
||||||
import run.halo.app.extension.controller.RequeueException;
|
import run.halo.app.extension.controller.RequeueException;
|
||||||
|
import run.halo.app.infra.Condition;
|
||||||
import run.halo.app.infra.ConditionStatus;
|
import run.halo.app.infra.ConditionStatus;
|
||||||
import run.halo.app.plugin.PluginProperties;
|
import run.halo.app.plugin.PluginProperties;
|
||||||
import run.halo.app.plugin.SpringPluginManager;
|
import run.halo.app.plugin.SpringPluginManager;
|
||||||
|
|
@ -122,11 +124,22 @@ class PluginReconcilerTest {
|
||||||
|
|
||||||
when(client.fetch(Plugin.class, name)).thenReturn(Optional.of(fakePlugin));
|
when(client.fetch(Plugin.class, name)).thenReturn(Optional.of(fakePlugin));
|
||||||
|
|
||||||
var t = assertThrows(IllegalStateException.class,
|
var result = reconciler.reconcile(new Request(name));
|
||||||
() -> reconciler.reconcile(new Request(name)));
|
assertFalse(result.reEnqueue());
|
||||||
assertEquals(
|
|
||||||
"Cannot run the plugin fake-plugin with dev mode in non-development environment.",
|
var status = fakePlugin.getStatus();
|
||||||
t.getMessage());
|
assertEquals(Plugin.Phase.UNKNOWN, status.getPhase());
|
||||||
|
var condition = status.getConditions().peekFirst();
|
||||||
|
assertEquals(Condition.builder()
|
||||||
|
.type(PluginReconciler.ConditionType.INITIALIZED)
|
||||||
|
.status(ConditionStatus.FALSE)
|
||||||
|
.reason(PluginReconciler.ConditionReason.INVALID_RUNTIME_MODE)
|
||||||
|
.message("""
|
||||||
|
Cannot run the plugin with development mode in non-development environment.\
|
||||||
|
""")
|
||||||
|
.build(), condition);
|
||||||
|
|
||||||
|
verify(client).update(fakePlugin);
|
||||||
verify(client).fetch(Plugin.class, name);
|
verify(client).fetch(Plugin.class, name);
|
||||||
verify(pluginProperties).getRuntimeMode();
|
verify(pluginProperties).getRuntimeMode();
|
||||||
verify(pluginManager, never()).loadPlugin(any(Path.class));
|
verify(pluginManager, never()).loadPlugin(any(Path.class));
|
||||||
|
|
@ -177,12 +190,8 @@ class PluginReconcilerTest {
|
||||||
.thenReturn(null);
|
.thenReturn(null);
|
||||||
when(pluginProperties.getRuntimeMode()).thenReturn(RuntimeMode.DEVELOPMENT);
|
when(pluginProperties.getRuntimeMode()).thenReturn(RuntimeMode.DEVELOPMENT);
|
||||||
|
|
||||||
var gotException = assertThrows(IllegalArgumentException.class,
|
var result = reconciler.reconcile(new Request(name));
|
||||||
() -> reconciler.reconcile(new Request(name)));
|
assertFalse(result.reEnqueue());
|
||||||
|
|
||||||
assertEquals("""
|
|
||||||
Please set plugin path annotation "plugin.halo.run/runtime-mode" in development \
|
|
||||||
mode for plugin fake-plugin.""", gotException.getMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -197,15 +206,18 @@ class PluginReconcilerTest {
|
||||||
|
|
||||||
when(client.fetch(Plugin.class, name)).thenReturn(Optional.of(fakePlugin));
|
when(client.fetch(Plugin.class, name)).thenReturn(Optional.of(fakePlugin));
|
||||||
when(pluginManager.getPluginsRoots()).thenReturn(List.of(tempPath));
|
when(pluginManager.getPluginsRoots()).thenReturn(List.of(tempPath));
|
||||||
when(pluginManager.getPlugin(name)).thenReturn(mock(PluginWrapper.class));
|
var pluginWrapper = mockPluginWrapper(PluginState.RESOLVED);
|
||||||
when(pluginManager.reloadPlugin(eq(name), any(Path.class))).thenReturn(true);
|
when(pluginManager.getPlugin(name)).thenReturn(pluginWrapper);
|
||||||
when(pluginManager.startPlugin(name)).thenReturn(PluginState.STARTED);
|
when(pluginManager.startPlugin(name)).thenReturn(PluginState.STARTED);
|
||||||
|
when(pluginManager.getUnresolvedPlugins()).thenReturn(List.of(pluginWrapper));
|
||||||
|
when(pluginManager.getResolvedPlugins()).thenReturn(List.of());
|
||||||
|
|
||||||
var result = reconciler.reconcile(new Request(name));
|
var result = reconciler.reconcile(new Request(name));
|
||||||
assertFalse(result.reEnqueue());
|
assertFalse(result.reEnqueue());
|
||||||
|
|
||||||
|
verify(pluginManager).unloadPlugin(name);
|
||||||
var loadLocation = Paths.get(fakePlugin.getStatus().getLoadLocation());
|
var loadLocation = Paths.get(fakePlugin.getStatus().getLoadLocation());
|
||||||
verify(pluginManager).reloadPlugin(name, loadLocation);
|
verify(pluginManager).loadPlugin(loadLocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -229,9 +241,19 @@ class PluginReconcilerTest {
|
||||||
.thenReturn(mockPluginWrapperForStaticResources());
|
.thenReturn(mockPluginWrapperForStaticResources());
|
||||||
when(pluginManager.startPlugin(name)).thenReturn(PluginState.FAILED);
|
when(pluginManager.startPlugin(name)).thenReturn(PluginState.FAILED);
|
||||||
|
|
||||||
var e = assertThrows(IllegalStateException.class,
|
var result = reconciler.reconcile(new Request(name));
|
||||||
() -> reconciler.reconcile(new Request(name)));
|
assertFalse(result.reEnqueue());
|
||||||
assertEquals("Failed to start plugin fake-plugin", e.getMessage());
|
|
||||||
|
verify(client).update(fakePlugin);
|
||||||
|
var status = fakePlugin.getStatus();
|
||||||
|
assertEquals(Plugin.Phase.FAILED, status.getPhase());
|
||||||
|
var condition = status.getConditions().peekFirst();
|
||||||
|
assertEquals(Condition.builder()
|
||||||
|
.type(PluginReconciler.ConditionType.READY)
|
||||||
|
.status(ConditionStatus.FALSE)
|
||||||
|
.reason(PluginReconciler.ConditionReason.START_ERROR)
|
||||||
|
.message("Failed to start plugin fake-plugin(FAILED).")
|
||||||
|
.build(), condition);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -253,6 +275,8 @@ class PluginReconcilerTest {
|
||||||
// get setting extension
|
// get setting extension
|
||||||
.thenReturn(mockPluginWrapperForSetting())
|
.thenReturn(mockPluginWrapperForSetting())
|
||||||
.thenReturn(mockPluginWrapperForStaticResources())
|
.thenReturn(mockPluginWrapperForStaticResources())
|
||||||
|
// before starting
|
||||||
|
.thenReturn(mockPluginWrapper(PluginState.STOPPED))
|
||||||
// sync plugin state
|
// sync plugin state
|
||||||
.thenReturn(mockPluginWrapper(PluginState.STARTED));
|
.thenReturn(mockPluginWrapper(PluginState.STARTED));
|
||||||
when(pluginManager.startPlugin(name)).thenReturn(PluginState.STARTED);
|
when(pluginManager.startPlugin(name)).thenReturn(PluginState.STARTED);
|
||||||
|
|
@ -277,13 +301,13 @@ class PluginReconcilerTest {
|
||||||
assertNotNull(fakePlugin.getStatus().getLastStartTime());
|
assertNotNull(fakePlugin.getStatus().getLastStartTime());
|
||||||
|
|
||||||
var condition = fakePlugin.getStatus().getConditions().peek();
|
var condition = fakePlugin.getStatus().getConditions().peek();
|
||||||
assertEquals("STARTED", condition.getType());
|
assertEquals(PluginReconciler.ConditionType.READY, condition.getType());
|
||||||
assertEquals(ConditionStatus.TRUE, condition.getStatus());
|
assertEquals(ConditionStatus.TRUE, condition.getStatus());
|
||||||
assertEquals(clock.instant(), condition.getLastTransitionTime());
|
assertEquals(clock.instant(), condition.getLastTransitionTime());
|
||||||
|
|
||||||
verify(pluginManager).startPlugin(name);
|
verify(pluginManager).startPlugin(name);
|
||||||
verify(pluginManager).loadPlugin(loadLocation);
|
verify(pluginManager).loadPlugin(loadLocation);
|
||||||
verify(pluginManager, times(4)).getPlugin(name);
|
verify(pluginManager, times(5)).getPlugin(name);
|
||||||
verify(client).update(fakePlugin);
|
verify(client).update(fakePlugin);
|
||||||
verify(client).fetch(Setting.class, settingName);
|
verify(client).fetch(Setting.class, settingName);
|
||||||
verify(client).create(any(Setting.class));
|
verify(client).create(any(Setting.class));
|
||||||
|
|
@ -354,8 +378,9 @@ class PluginReconcilerTest {
|
||||||
|
|
||||||
var pluginRootResource =
|
var pluginRootResource =
|
||||||
new DefaultResourceLoader().getResource("classpath:plugin/plugin-0.0.1/");
|
new DefaultResourceLoader().getResource("classpath:plugin/plugin-0.0.1/");
|
||||||
var classLoader = new URLClassLoader(new URL[]{pluginRootResource.getURL()}, null);
|
var classLoader = new URLClassLoader(new URL[] {pluginRootResource.getURL()}, null);
|
||||||
when(pluginWrapper.getPluginClassLoader()).thenReturn(classLoader);
|
when(pluginWrapper.getPluginClassLoader()).thenReturn(classLoader);
|
||||||
|
lenient().when(pluginWrapper.getDescriptor()).thenReturn(new DefaultPluginDescriptor());
|
||||||
return pluginWrapper;
|
return pluginWrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -368,12 +393,14 @@ class PluginReconcilerTest {
|
||||||
when(pluginClassLoader.getResource("console/style.css")).thenReturn(
|
when(pluginClassLoader.getResource("console/style.css")).thenReturn(
|
||||||
mock(URL.class));
|
mock(URL.class));
|
||||||
when(pluginWrapper.getPluginClassLoader()).thenReturn(pluginClassLoader);
|
when(pluginWrapper.getPluginClassLoader()).thenReturn(pluginClassLoader);
|
||||||
|
lenient().when(pluginWrapper.getDescriptor()).thenReturn(new DefaultPluginDescriptor());
|
||||||
return pluginWrapper;
|
return pluginWrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
PluginWrapper mockPluginWrapper(PluginState state) {
|
PluginWrapper mockPluginWrapper(PluginState state) {
|
||||||
var pluginWrapper = mock(PluginWrapper.class);
|
var pluginWrapper = mock(PluginWrapper.class);
|
||||||
when(pluginWrapper.getPluginState()).thenReturn(state);
|
lenient().when(pluginWrapper.getPluginState()).thenReturn(state);
|
||||||
|
lenient().when(pluginWrapper.getDescriptor()).thenReturn(new DefaultPluginDescriptor());
|
||||||
return pluginWrapper;
|
return pluginWrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package run.halo.app.core.extension.service.impl;
|
||||||
import static java.util.Objects.requireNonNull;
|
import static java.util.Objects.requireNonNull;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.isA;
|
import static org.mockito.ArgumentMatchers.isA;
|
||||||
|
|
@ -40,9 +41,9 @@ import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.infra.SystemVersionSupplier;
|
import run.halo.app.infra.SystemVersionSupplier;
|
||||||
import run.halo.app.infra.exception.PluginAlreadyExistsException;
|
import run.halo.app.infra.exception.PluginAlreadyExistsException;
|
||||||
import run.halo.app.infra.utils.FileUtils;
|
import run.halo.app.infra.utils.FileUtils;
|
||||||
import run.halo.app.plugin.HaloPluginManager;
|
|
||||||
import run.halo.app.plugin.PluginConst;
|
import run.halo.app.plugin.PluginConst;
|
||||||
import run.halo.app.plugin.PluginProperties;
|
import run.halo.app.plugin.PluginProperties;
|
||||||
|
import run.halo.app.plugin.SpringPluginManager;
|
||||||
import run.halo.app.plugin.YamlPluginFinder;
|
import run.halo.app.plugin.YamlPluginFinder;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
|
@ -58,7 +59,7 @@ class PluginServiceImplTest {
|
||||||
PluginProperties pluginProperties;
|
PluginProperties pluginProperties;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
HaloPluginManager pluginManager;
|
SpringPluginManager pluginManager;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
PluginServiceImpl pluginService;
|
PluginServiceImpl pluginService;
|
||||||
|
|
@ -233,15 +234,6 @@ class PluginServiceImplTest {
|
||||||
verify(client).update(testPlugin);
|
verify(client).update(testPlugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
Plugin createPlugin(String name, Consumer<Plugin> pluginConsumer) {
|
|
||||||
var plugin = new Plugin();
|
|
||||||
plugin.setMetadata(new Metadata());
|
|
||||||
plugin.getMetadata().setName(name);
|
|
||||||
plugin.setSpec(new Plugin.PluginSpec());
|
|
||||||
plugin.setStatus(new Plugin.PluginStatus());
|
|
||||||
pluginConsumer.accept(plugin);
|
|
||||||
return plugin;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -299,4 +291,61 @@ class PluginServiceImplTest {
|
||||||
|
|
||||||
assertThat(result).isNotEqualTo(result2);
|
assertThat(result).isNotEqualTo(result2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class PluginStateChangeTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldEnablePluginIfPluginWasNotStarted() {
|
||||||
|
var plugin = createPlugin("fake-plugin", p -> {
|
||||||
|
p.getSpec().setEnabled(false);
|
||||||
|
p.statusNonNull().setPhase(Plugin.Phase.RESOLVED);
|
||||||
|
});
|
||||||
|
|
||||||
|
when(client.get(Plugin.class, "fake-plugin")).thenReturn(Mono.just(plugin))
|
||||||
|
.thenReturn(Mono.fromSupplier(() -> {
|
||||||
|
plugin.statusNonNull().setPhase(Plugin.Phase.STARTED);
|
||||||
|
return plugin;
|
||||||
|
}));
|
||||||
|
when(client.update(plugin)).thenReturn(Mono.just(plugin));
|
||||||
|
|
||||||
|
pluginService.changeState("fake-plugin", true, false)
|
||||||
|
.as(StepVerifier::create)
|
||||||
|
.expectNext(plugin)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
assertTrue(plugin.getSpec().getEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDisablePluginIfAlreadyStarted() {
|
||||||
|
var plugin = createPlugin("fake-plugin", p -> {
|
||||||
|
p.getSpec().setEnabled(true);
|
||||||
|
p.statusNonNull().setPhase(Plugin.Phase.STARTED);
|
||||||
|
});
|
||||||
|
|
||||||
|
when(client.get(Plugin.class, "fake-plugin")).thenReturn(Mono.just(plugin))
|
||||||
|
.thenReturn(Mono.fromSupplier(() -> {
|
||||||
|
plugin.getStatus().setPhase(Plugin.Phase.STOPPED);
|
||||||
|
return plugin;
|
||||||
|
}));
|
||||||
|
when(client.update(plugin)).thenReturn(Mono.just(plugin));
|
||||||
|
|
||||||
|
pluginService.changeState("fake-plugin", false, false)
|
||||||
|
.as(StepVerifier::create)
|
||||||
|
.expectNext(plugin)
|
||||||
|
.verifyComplete();
|
||||||
|
assertFalse(plugin.getSpec().getEnabled());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Plugin createPlugin(String name, Consumer<Plugin> pluginConsumer) {
|
||||||
|
var plugin = new Plugin();
|
||||||
|
plugin.setMetadata(new Metadata());
|
||||||
|
plugin.getMetadata().setName(name);
|
||||||
|
plugin.setSpec(new Plugin.PluginSpec());
|
||||||
|
plugin.setStatus(new Plugin.PluginStatus());
|
||||||
|
pluginConsumer.accept(plugin);
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
package run.halo.app.infra;
|
package run.halo.app.infra;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
@ -142,6 +145,86 @@ class ConditionListTest {
|
||||||
assertThat(conditions.peek().getType()).isEqualTo("type3");
|
assertThat(conditions.peek().getType()).isEqualTo("type3");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotAddIfTypeIsSame() {
|
||||||
|
var conditions = new ConditionList();
|
||||||
|
var condition = Condition.builder()
|
||||||
|
.type("type")
|
||||||
|
.status(ConditionStatus.TRUE)
|
||||||
|
.reason("reason")
|
||||||
|
.message("message")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
var anotherCondition = Condition.builder()
|
||||||
|
.type("type")
|
||||||
|
.status(ConditionStatus.FALSE)
|
||||||
|
.reason("another reason")
|
||||||
|
.message("another message")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
conditions.addAndEvictFIFO(condition);
|
||||||
|
conditions.addAndEvictFIFO(anotherCondition);
|
||||||
|
|
||||||
|
assertEquals(1, conditions.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotUpdateLastTransitionTimeIfStatusNotChanged() {
|
||||||
|
var now = Instant.now();
|
||||||
|
var conditions = new ConditionList();
|
||||||
|
conditions.addAndEvictFIFO(
|
||||||
|
Condition.builder()
|
||||||
|
.type("type")
|
||||||
|
.status(ConditionStatus.TRUE)
|
||||||
|
.reason("reason")
|
||||||
|
.message("message")
|
||||||
|
.lastTransitionTime(now)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
conditions.addAndEvictFIFO(
|
||||||
|
Condition.builder()
|
||||||
|
.type("type")
|
||||||
|
.status(ConditionStatus.TRUE)
|
||||||
|
.reason("reason")
|
||||||
|
.message("message")
|
||||||
|
.lastTransitionTime(now.plus(Duration.ofSeconds(1)))
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(1, conditions.size());
|
||||||
|
// make sure the last transition time was not modified.
|
||||||
|
assertEquals(now, conditions.peek().getLastTransitionTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUpdateLastTransitionTimeIfStatusChanged() {
|
||||||
|
var now = Instant.now();
|
||||||
|
var conditions = new ConditionList();
|
||||||
|
conditions.addAndEvictFIFO(
|
||||||
|
Condition.builder()
|
||||||
|
.type("type")
|
||||||
|
.status(ConditionStatus.TRUE)
|
||||||
|
.reason("reason")
|
||||||
|
.message("message")
|
||||||
|
.lastTransitionTime(now)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
conditions.addAndEvictFIFO(
|
||||||
|
Condition.builder()
|
||||||
|
.type("type")
|
||||||
|
.status(ConditionStatus.FALSE)
|
||||||
|
.reason("reason")
|
||||||
|
.message("message")
|
||||||
|
.lastTransitionTime(now.plus(Duration.ofSeconds(1)))
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(1, conditions.size());
|
||||||
|
assertEquals(now.plus(Duration.ofSeconds(1)), conditions.peek().getLastTransitionTime());
|
||||||
|
}
|
||||||
|
|
||||||
private Condition condition(String type, String message, String reason,
|
private Condition condition(String type, String message, String reason,
|
||||||
ConditionStatus status) {
|
ConditionStatus status) {
|
||||||
Condition condition = new Condition();
|
Condition condition = new Condition();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { VButton, VModal } from "@halo-dev/components";
|
||||||
|
import type { Plugin } from "@halo-dev/api-client";
|
||||||
|
import { formatDatetime, relativeTimeTo } from "@/utils/date";
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
withDefaults(defineProps<{ plugin: Plugin }>(), {});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "close"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const modal = ref();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VModal
|
||||||
|
ref="modal"
|
||||||
|
:body-class="['!p-0']"
|
||||||
|
:title="$t('core.plugin.conditions_modal.title')"
|
||||||
|
:width="900"
|
||||||
|
layer-closable
|
||||||
|
@close="emit('close')"
|
||||||
|
>
|
||||||
|
<table class="min-w-full divide-y divide-gray-100">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
class="px-4 py-3 text-left text-sm font-semibold text-gray-900 sm:w-96"
|
||||||
|
scope="col"
|
||||||
|
>
|
||||||
|
{{ $t("core.plugin.conditions_modal.fields.type") }}
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="px-4 py-3 text-left text-sm font-semibold text-gray-900"
|
||||||
|
>
|
||||||
|
{{ $t("core.plugin.conditions_modal.fields.status") }}
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="px-4 py-3 text-left text-sm font-semibold text-gray-900"
|
||||||
|
>
|
||||||
|
{{ $t("core.plugin.conditions_modal.fields.reason") }}
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="px-4 py-3 text-left text-sm font-semibold text-gray-900"
|
||||||
|
>
|
||||||
|
{{ $t("core.plugin.conditions_modal.fields.message") }}
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="px-4 py-3 text-left text-sm font-semibold text-gray-900"
|
||||||
|
>
|
||||||
|
{{ $t("core.plugin.conditions_modal.fields.last_transition_time") }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-100 bg-white">
|
||||||
|
<tr
|
||||||
|
v-for="(condition, index) in plugin?.status?.conditions"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900">
|
||||||
|
{{ condition.type }}
|
||||||
|
</td>
|
||||||
|
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-500">
|
||||||
|
{{ condition.status }}
|
||||||
|
</td>
|
||||||
|
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-500">
|
||||||
|
{{ condition.reason || "-" }}
|
||||||
|
</td>
|
||||||
|
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-500">
|
||||||
|
{{ condition.message || "-" }}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
v-tooltip="formatDatetime(condition.lastTransitionTime)"
|
||||||
|
class="whitespace-nowrap px-4 py-3 text-sm text-gray-500"
|
||||||
|
>
|
||||||
|
{{ relativeTimeTo(condition.lastTransitionTime) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<VButton @click="modal.close()">
|
||||||
|
{{ $t("core.common.buttons.close") }}
|
||||||
|
</VButton>
|
||||||
|
</template>
|
||||||
|
</VModal>
|
||||||
|
</template>
|
||||||
|
|
@ -43,7 +43,8 @@ const { plugin } = toRefs(props);
|
||||||
|
|
||||||
const selectedNames = inject<Ref<string[]>>("selectedNames", ref([]));
|
const selectedNames = inject<Ref<string[]>>("selectedNames", ref([]));
|
||||||
|
|
||||||
const { getStatusMessage, uninstall } = usePluginLifeCycle(plugin);
|
const { getStatusDotState, getStatusMessage, uninstall } =
|
||||||
|
usePluginLifeCycle(plugin);
|
||||||
|
|
||||||
const pluginUpgradeModalVisible = ref(false);
|
const pluginUpgradeModalVisible = ref(false);
|
||||||
|
|
||||||
|
|
@ -147,19 +148,11 @@ const { startFields, endFields } = useEntityFieldItemExtensionPoint<Plugin>(
|
||||||
"plugin:list-item:field:create",
|
"plugin:list-item:field:create",
|
||||||
plugin,
|
plugin,
|
||||||
computed((): EntityFieldItem[] => {
|
computed((): EntityFieldItem[] => {
|
||||||
const { enabled } = props.plugin.spec || {};
|
|
||||||
const { phase } = props.plugin.status || {};
|
const { phase } = props.plugin.status || {};
|
||||||
|
|
||||||
const shouldHideStatusDot =
|
const shouldHideStatusDot =
|
||||||
!enabled || (enabled && phase === PluginStatusPhaseEnum.Started);
|
phase === PluginStatusPhaseEnum.Started ||
|
||||||
|
phase === PluginStatusPhaseEnum.Disabled;
|
||||||
const getStatusDotState = () => {
|
|
||||||
if (enabled && phase === PluginStatusPhaseEnum.Failed) {
|
|
||||||
return "error";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "default";
|
|
||||||
};
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { useMutation } from "@tanstack/vue-query";
|
||||||
|
|
||||||
interface usePluginLifeCycleReturn {
|
interface usePluginLifeCycleReturn {
|
||||||
isStarted: ComputedRef<boolean | undefined>;
|
isStarted: ComputedRef<boolean | undefined>;
|
||||||
|
getStatusDotState: () => string;
|
||||||
getStatusMessage: () => string | undefined;
|
getStatusMessage: () => string | undefined;
|
||||||
changeStatus: () => void;
|
changeStatus: () => void;
|
||||||
changingStatus: Ref<boolean>;
|
changingStatus: Ref<boolean>;
|
||||||
|
|
@ -27,26 +28,41 @@ export function usePluginLifeCycle(
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getStatusDotState = () => {
|
||||||
|
const { phase } = plugin?.value?.status || {};
|
||||||
|
const { enabled } = plugin?.value?.spec || {};
|
||||||
|
|
||||||
|
if (enabled && phase === PluginStatusPhaseEnum.Failed) {
|
||||||
|
return "error";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phase === PluginStatusPhaseEnum.Disabling) {
|
||||||
|
return "warning";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "default";
|
||||||
|
};
|
||||||
|
|
||||||
const getStatusMessage = () => {
|
const getStatusMessage = () => {
|
||||||
if (!plugin?.value) return;
|
if (!plugin?.value) return;
|
||||||
|
|
||||||
const { enabled } = plugin.value.spec || {};
|
|
||||||
const { phase } = plugin.value.status || {};
|
const { phase } = plugin.value.status || {};
|
||||||
|
|
||||||
// Starting failed
|
if (
|
||||||
if (enabled && phase === PluginStatusPhaseEnum.Failed) {
|
phase === PluginStatusPhaseEnum.Failed ||
|
||||||
|
phase === PluginStatusPhaseEnum.Disabling
|
||||||
|
) {
|
||||||
const lastCondition = plugin.value.status?.conditions?.[0];
|
const lastCondition = plugin.value.status?.conditions?.[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
[lastCondition?.reason, lastCondition?.message]
|
[lastCondition?.reason, lastCondition?.message]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(":") || "Unknown"
|
.join(": ") || "Unknown"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Starting up
|
// Starting up
|
||||||
if (
|
if (
|
||||||
enabled &&
|
|
||||||
phase !== (PluginStatusPhaseEnum.Started || PluginStatusPhaseEnum.Failed)
|
phase !== (PluginStatusPhaseEnum.Started || PluginStatusPhaseEnum.Failed)
|
||||||
) {
|
) {
|
||||||
return t("core.common.status.starting_up");
|
return t("core.common.status.starting_up");
|
||||||
|
|
@ -153,6 +169,7 @@ export function usePluginLifeCycle(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isStarted,
|
isStarted,
|
||||||
|
getStatusDotState,
|
||||||
getStatusMessage,
|
getStatusMessage,
|
||||||
changeStatus,
|
changeStatus,
|
||||||
changingStatus,
|
changingStatus,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {
|
import {
|
||||||
VAlert,
|
VAlert,
|
||||||
|
VButton,
|
||||||
VDescription,
|
VDescription,
|
||||||
VDescriptionItem,
|
VDescriptionItem,
|
||||||
VSwitch,
|
VSwitch,
|
||||||
|
|
@ -8,12 +9,18 @@ import {
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
import { computed, inject } from "vue";
|
import { computed, inject } from "vue";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import type { Plugin, Role } from "@halo-dev/api-client";
|
import {
|
||||||
|
PluginStatusPhaseEnum,
|
||||||
|
type Plugin,
|
||||||
|
type Role,
|
||||||
|
} from "@halo-dev/api-client";
|
||||||
import { pluginLabels, roleLabels } from "@/constants/labels";
|
import { pluginLabels, roleLabels } from "@/constants/labels";
|
||||||
import { rbacAnnotations } from "@/constants/annotations";
|
import { rbacAnnotations } from "@/constants/annotations";
|
||||||
import { usePluginLifeCycle } from "../composables/use-plugin";
|
import { usePluginLifeCycle } from "../composables/use-plugin";
|
||||||
import { formatDatetime } from "@/utils/date";
|
import { formatDatetime } from "@/utils/date";
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import PluginConditionsModal from "../components/PluginConditionsModal.vue";
|
||||||
|
|
||||||
const plugin = inject<Ref<Plugin | undefined>>("plugin");
|
const plugin = inject<Ref<Plugin | undefined>>("plugin");
|
||||||
const { changeStatus, changingStatus } = usePluginLifeCycle(plugin);
|
const { changeStatus, changingStatus } = usePluginLifeCycle(plugin);
|
||||||
|
|
@ -60,9 +67,30 @@ const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
|
||||||
});
|
});
|
||||||
return groups;
|
return groups;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Error alert
|
||||||
|
const conditionsModalVisible = ref(false);
|
||||||
|
|
||||||
|
const errorAlertVisible = computed(() => {
|
||||||
|
const { phase } = plugin?.value?.status || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
phase !== PluginStatusPhaseEnum.Started &&
|
||||||
|
phase !== PluginStatusPhaseEnum.Disabled
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const lastCondition = computed(() => {
|
||||||
|
return plugin?.value?.status?.conditions?.[0];
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<PluginConditionsModal
|
||||||
|
v-if="conditionsModalVisible && plugin"
|
||||||
|
:plugin="plugin"
|
||||||
|
@close="conditionsModalVisible = false"
|
||||||
|
/>
|
||||||
<Transition mode="out-in" name="fade">
|
<Transition mode="out-in" name="fade">
|
||||||
<div class="overflow-hidden rounded-b-base">
|
<div class="overflow-hidden rounded-b-base">
|
||||||
<div class="flex items-center justify-between bg-white px-4 py-4 sm:px-6">
|
<div class="flex items-center justify-between bg-white px-4 py-4 sm:px-6">
|
||||||
|
|
@ -80,18 +108,21 @@ const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="errorAlertVisible && lastCondition"
|
||||||
plugin?.status?.phase === 'FAILED' &&
|
|
||||||
plugin?.status?.conditions?.length
|
|
||||||
"
|
|
||||||
class="w-full px-4 pb-2 sm:px-6"
|
class="w-full px-4 pb-2 sm:px-6"
|
||||||
>
|
>
|
||||||
<VAlert
|
<VAlert
|
||||||
type="error"
|
type="error"
|
||||||
:title="plugin?.status?.conditions?.[0].reason"
|
:title="lastCondition.reason"
|
||||||
:description="plugin?.status?.conditions?.[0].message"
|
:description="lastCondition.message"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
/>
|
>
|
||||||
|
<template #actions>
|
||||||
|
<VButton size="sm" @click="conditionsModalVisible = true">
|
||||||
|
{{ $t("core.plugin.detail.operations.view_conditions.button") }}
|
||||||
|
</VButton>
|
||||||
|
</template>
|
||||||
|
</VAlert>
|
||||||
</div>
|
</div>
|
||||||
<div class="border-t border-gray-200">
|
<div class="border-t border-gray-200">
|
||||||
<VDescription>
|
<VDescription>
|
||||||
|
|
|
||||||
|
|
@ -31,13 +31,13 @@ export interface Condition {
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof Condition
|
* @memberof Condition
|
||||||
*/
|
*/
|
||||||
'message': string;
|
'message'?: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof Condition
|
* @memberof Condition
|
||||||
*/
|
*/
|
||||||
'reason': string;
|
'reason'?: string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,7 @@ export const PluginStatusPhaseEnum = {
|
||||||
Pending: 'PENDING',
|
Pending: 'PENDING',
|
||||||
Starting: 'STARTING',
|
Starting: 'STARTING',
|
||||||
Created: 'CREATED',
|
Created: 'CREATED',
|
||||||
|
Disabling: 'DISABLING',
|
||||||
Disabled: 'DISABLED',
|
Disabled: 'DISABLED',
|
||||||
Resolved: 'RESOLVED',
|
Resolved: 'RESOLVED',
|
||||||
Started: 'STARTED',
|
Started: 'STARTED',
|
||||||
|
|
|
||||||
|
|
@ -908,6 +908,9 @@ core:
|
||||||
repo: Source Repository
|
repo: Source Repository
|
||||||
load_location: Storage Location
|
load_location: Storage Location
|
||||||
issues: Issues feedback
|
issues: Issues feedback
|
||||||
|
operations:
|
||||||
|
view_conditions:
|
||||||
|
button: View recent conditions
|
||||||
loader:
|
loader:
|
||||||
toast:
|
toast:
|
||||||
entry_load_failed: Failed to load plugins entry file
|
entry_load_failed: Failed to load plugins entry file
|
||||||
|
|
@ -916,6 +919,14 @@ core:
|
||||||
editor:
|
editor:
|
||||||
providers:
|
providers:
|
||||||
default: Default Editor
|
default: Default Editor
|
||||||
|
conditions_modal:
|
||||||
|
title: Recent conditions
|
||||||
|
fields:
|
||||||
|
type: Type
|
||||||
|
status: Status
|
||||||
|
reason: Reason
|
||||||
|
message: Message
|
||||||
|
last_transition_time: Last transition time
|
||||||
user:
|
user:
|
||||||
title: Users
|
title: Users
|
||||||
actions:
|
actions:
|
||||||
|
|
|
||||||
|
|
@ -864,6 +864,9 @@ core:
|
||||||
repo: 源码仓库
|
repo: 源码仓库
|
||||||
load_location: 存储位置
|
load_location: 存储位置
|
||||||
issues: 问题反馈
|
issues: 问题反馈
|
||||||
|
operations:
|
||||||
|
view_conditions:
|
||||||
|
button: 查看最近状态
|
||||||
loader:
|
loader:
|
||||||
toast:
|
toast:
|
||||||
entry_load_failed: 加载插件入口文件失败
|
entry_load_failed: 加载插件入口文件失败
|
||||||
|
|
@ -872,6 +875,14 @@ core:
|
||||||
editor:
|
editor:
|
||||||
providers:
|
providers:
|
||||||
default: 默认编辑器
|
default: 默认编辑器
|
||||||
|
conditions_modal:
|
||||||
|
title: 插件最近状态
|
||||||
|
fields:
|
||||||
|
type: 类型
|
||||||
|
status: 状态
|
||||||
|
reason: 原因
|
||||||
|
message: 信息
|
||||||
|
last_transition_time: 时间
|
||||||
user:
|
user:
|
||||||
title: 用户
|
title: 用户
|
||||||
actions:
|
actions:
|
||||||
|
|
|
||||||
|
|
@ -844,6 +844,9 @@ core:
|
||||||
repo: 源碼倉庫
|
repo: 源碼倉庫
|
||||||
load_location: 存儲位置
|
load_location: 存儲位置
|
||||||
issues: 問題回饋
|
issues: 問題回饋
|
||||||
|
operations:
|
||||||
|
view_conditions:
|
||||||
|
button: 查看最近狀態
|
||||||
loader:
|
loader:
|
||||||
toast:
|
toast:
|
||||||
entry_load_failed: 讀取插件入口文件失敗
|
entry_load_failed: 讀取插件入口文件失敗
|
||||||
|
|
@ -852,6 +855,14 @@ core:
|
||||||
editor:
|
editor:
|
||||||
providers:
|
providers:
|
||||||
default: 預設編輯器
|
default: 預設編輯器
|
||||||
|
conditions_modal:
|
||||||
|
title: 插件最近狀態
|
||||||
|
fields:
|
||||||
|
type: 類型
|
||||||
|
status: 狀態
|
||||||
|
reason: 原因
|
||||||
|
message: 訊息
|
||||||
|
last_transition_time: 時間
|
||||||
user:
|
user:
|
||||||
title: 用戶
|
title: 用戶
|
||||||
actions:
|
actions:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue