mirror of https://github.com/halo-dev/halo
feat: plugin supports configuration when not started (#3355)
#### What type of PR is this? /kind feature /milestone 2.3.x /area core /area plugin #### What this PR does / why we need it: 顺便重构了一下插件 Reconciler ,否则经常容易出 bug,刚好一起测试一下免得反复整体测试这一块。 1. 插件 status 里的 message 和 reason 之前是直接放在 status 里的,现在改为一个 ConditionList, 最新的始终在 ConditionList 的第一个,这可以避免插件启动成功时还能在 message 上看到错误信息会很奇怪 2. 插件的 Setting 只会在卸载时删除,停止时不进行删除 3. 插件启动和停止状态的调和能从 CREATED、DISABLED、RESOLVED、STARTED、STOPPED、FAILED 这些状态之间进行循环过渡,如插件当前状态为 CREATED 期望达到 STARTED 则可能会经历 CREATED -> RESOLVED -> STARTED( -> FAILED) 这些过渡, 逻辑更清晰 Console 需要适配: 1. 需要去掉插件启动时才加载 Setting 的判断,让其在插件停用时也能加载 2. 需要修改发生错误时的字段取值,显示 conditons[0] #### Which issue(s) this PR fixes: Fixes #3352 #### Special notes for your reviewer: how to test it? 1. 测试在开发模式和生产模式下插件启用/设置/配置是否正常 2. 测试开发模式下插件启动失败后修复问题重启 halo 后能否正常启用 3. 测试插件卸载后 setting 是否被正确删除 4. 测试插件 logo 相对路径和 url 是否都能正常显示 5. 测试评论插件启动成功后主题端是否能显示评论列表(验证扩展点加载是否正常) 6. 测试插件配置了 settingName 但没有对应的 Setting 时的情况看 Reconciler 重试时间是否增长 /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? ```release-note 允许插件在未启动时更改设置 ```pull/3379/head
parent
03ec23f90f
commit
6c2064f1e0
|
@ -13,8 +13,10 @@ import lombok.EqualsAndHashCode;
|
|||
import lombok.ToString;
|
||||
import org.pf4j.PluginState;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.util.Assert;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.GVK;
|
||||
import run.halo.app.infra.ConditionList;
|
||||
|
||||
/**
|
||||
* A custom resource for Plugin.
|
||||
|
@ -102,19 +104,23 @@ public class Plugin extends AbstractExtension {
|
|||
|
||||
private PluginState phase;
|
||||
|
||||
private String reason;
|
||||
|
||||
private String message;
|
||||
private ConditionList conditions;
|
||||
|
||||
private Instant lastStartTime;
|
||||
|
||||
private Instant lastTransitionTime;
|
||||
|
||||
private String entry;
|
||||
|
||||
private String stylesheet;
|
||||
|
||||
private String logo;
|
||||
|
||||
public static ConditionList nullSafeConditions(@NonNull PluginStatus status) {
|
||||
Assert.notNull(status, "The status must not be null.");
|
||||
if (status.getConditions() == null) {
|
||||
status.setConditions(new ConditionList());
|
||||
}
|
||||
return status.getConditions();
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
|
|
|
@ -1,25 +1,35 @@
|
|||
package run.halo.app.core.extension.reconciler;
|
||||
|
||||
import static run.halo.app.plugin.PluginConst.DELETE_STAGE;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.pf4j.PluginRuntimeException;
|
||||
import org.pf4j.PluginDescriptor;
|
||||
import org.pf4j.PluginState;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.pf4j.RuntimeMode;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import run.halo.app.core.extension.Plugin;
|
||||
|
@ -28,15 +38,22 @@ import run.halo.app.core.extension.Setting;
|
|||
import run.halo.app.core.extension.theme.SettingUtils;
|
||||
import run.halo.app.extension.ConfigMap;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.Unstructured;
|
||||
import run.halo.app.extension.controller.Controller;
|
||||
import run.halo.app.extension.controller.ControllerBuilder;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.extension.controller.Reconciler.Request;
|
||||
import run.halo.app.infra.Condition;
|
||||
import run.halo.app.infra.ConditionStatus;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.infra.utils.PathUtils;
|
||||
import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
||||
import run.halo.app.plugin.HaloPluginManager;
|
||||
import run.halo.app.plugin.PluginConst;
|
||||
import run.halo.app.plugin.PluginExtensionLoaderUtils;
|
||||
import run.halo.app.plugin.PluginNotFoundException;
|
||||
import run.halo.app.plugin.PluginStartingError;
|
||||
import run.halo.app.plugin.event.PluginCreatedEvent;
|
||||
|
@ -56,20 +73,367 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
private final ExtensionClient client;
|
||||
private final HaloPluginManager haloPluginManager;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
private final RetryTemplate retryTemplate = RetryTemplate.builder()
|
||||
.maxAttempts(20)
|
||||
.fixedBackoff(300)
|
||||
.retryOn(IllegalStateException.class)
|
||||
.build();
|
||||
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
client.fetch(Plugin.class, request.name())
|
||||
.ifPresent(plugin -> {
|
||||
return client.fetch(Plugin.class, request.name())
|
||||
.map(plugin -> {
|
||||
if (plugin.getMetadata().getDeletionTimestamp() != null) {
|
||||
cleanUpResourcesAndRemoveFinalizer(request.name());
|
||||
return;
|
||||
return Result.doNotRetry();
|
||||
}
|
||||
addFinalizerIfNecessary(plugin);
|
||||
|
||||
// if true returned, it means it is not ready
|
||||
if (readinessDetection(request.name())) {
|
||||
return new Result(true, null);
|
||||
}
|
||||
|
||||
reconcilePluginState(plugin.getMetadata().getName());
|
||||
return Result.doNotRetry();
|
||||
})
|
||||
.orElse(Result.doNotRetry());
|
||||
}
|
||||
|
||||
boolean readinessDetection(String name) {
|
||||
return client.fetch(Plugin.class, name)
|
||||
.map(plugin -> {
|
||||
if (waitForSettingCreation(plugin)) {
|
||||
return true;
|
||||
}
|
||||
createInitialReverseProxyIfNotPresent(plugin);
|
||||
|
||||
Plugin.PluginStatus status = plugin.statusNonNull();
|
||||
|
||||
// filled logo path
|
||||
String logo = plugin.getSpec().getLogo();
|
||||
if (PathUtils.isAbsoluteUri(logo)) {
|
||||
status.setLogo(logo);
|
||||
} else {
|
||||
String assetsPrefix =
|
||||
PluginConst.assertsRoutePrefix(plugin.getMetadata().getName());
|
||||
status.setLogo(PathUtils.combinePath(assetsPrefix, logo));
|
||||
}
|
||||
|
||||
// update phase
|
||||
PluginWrapper pluginWrapper = getPluginWrapper(name);
|
||||
status.setPhase(pluginWrapper.getPluginState());
|
||||
updateStatus(plugin.getMetadata().getName(), status);
|
||||
return false;
|
||||
})
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
Optional<Setting> lookupPluginSetting(String name, String settingName) {
|
||||
Assert.notNull(name, "Plugin name must not be null");
|
||||
Assert.notNull(settingName, "Setting name must not be null");
|
||||
PluginWrapper pluginWrapper = getPluginWrapper(name);
|
||||
// If it already exists, do not look for setting
|
||||
if (RuntimeMode.DEPLOYMENT.equals(pluginWrapper.getRuntimeMode())) {
|
||||
Optional<Setting> existing = client.fetch(Setting.class, settingName);
|
||||
if (existing.isPresent()) {
|
||||
return existing;
|
||||
}
|
||||
}
|
||||
|
||||
var resourceLoader =
|
||||
new DefaultResourceLoader(pluginWrapper.getPluginClassLoader());
|
||||
return PluginExtensionLoaderUtils.lookupExtensions(pluginWrapper.getPluginPath(),
|
||||
pluginWrapper.getRuntimeMode())
|
||||
.stream()
|
||||
.map(resourceLoader::getResource)
|
||||
.filter(Resource::exists)
|
||||
.map(resource -> new YamlUnstructuredLoader(resource).load())
|
||||
.flatMap(Collection::stream)
|
||||
.filter(unstructured -> {
|
||||
GroupVersionKind groupVersionKind =
|
||||
GroupVersionKind.fromAPIVersionAndKind(unstructured.getApiVersion(),
|
||||
unstructured.getKind());
|
||||
GroupVersionKind settingGvk = GroupVersionKind.fromExtension(Setting.class);
|
||||
return settingGvk.groupKind().equals(groupVersionKind.groupKind())
|
||||
&& settingName.equals(unstructured.getMetadata().getName());
|
||||
})
|
||||
.findFirst()
|
||||
.map(unstructured -> Unstructured.OBJECT_MAPPER.convertValue(unstructured,
|
||||
Setting.class));
|
||||
}
|
||||
|
||||
boolean waitForSettingCreation(Plugin plugin) {
|
||||
final String pluginName = plugin.getMetadata().getName();
|
||||
|
||||
final String settingName = plugin.getSpec().getSettingName();
|
||||
if (StringUtils.isBlank(settingName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Optional<Setting> settingOption = lookupPluginSetting(pluginName, settingName)
|
||||
.map(setting -> {
|
||||
// This annotation is added to prevent it from being deleted when stopped.
|
||||
Map<String, String> settingAnnotations = ExtensionUtil.nullSafeAnnotations(setting);
|
||||
settingAnnotations.put(DELETE_STAGE, PluginConst.DeleteStage.UNINSTALL.name());
|
||||
return setting;
|
||||
})
|
||||
.map(settingFromYaml -> {
|
||||
client.fetch(Setting.class, settingName)
|
||||
.ifPresentOrElse(setting -> {
|
||||
settingFromYaml.getMetadata()
|
||||
.setVersion(setting.getMetadata().getVersion());
|
||||
client.update(settingFromYaml);
|
||||
}, () -> client.create(settingFromYaml));
|
||||
return settingFromYaml;
|
||||
});
|
||||
return new Result(false, null);
|
||||
|
||||
// Fix gh-3224
|
||||
// Maybe Setting is being created and cannot be queried. so try again.
|
||||
if (settingOption.isEmpty()) {
|
||||
Plugin.PluginStatus status = plugin.statusNonNull();
|
||||
status.setPhase(PluginState.FAILED);
|
||||
var condition = Condition.builder()
|
||||
.type("BackOff")
|
||||
.reason("BackOff")
|
||||
.message("Wait for setting [" + settingName + "] creation")
|
||||
.status(ConditionStatus.FALSE)
|
||||
.lastTransitionTime(Instant.now())
|
||||
.build();
|
||||
Plugin.PluginStatus.nullSafeConditions(status)
|
||||
.addAndEvictFIFO(condition);
|
||||
updateStatus(plugin.getMetadata().getName(), status);
|
||||
// need requeue
|
||||
return true;
|
||||
}
|
||||
|
||||
final String configMapNameToUse = plugin.getSpec().getConfigMapName();
|
||||
if (StringUtils.isBlank(configMapNameToUse)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean existConfigMap = client.fetch(ConfigMap.class, configMapNameToUse)
|
||||
.isPresent();
|
||||
if (existConfigMap) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var data = SettingUtils.settingDefinedDefaultValueMap(settingOption.get());
|
||||
// Create with or without default value
|
||||
ConfigMap configMap = new ConfigMap();
|
||||
configMap.setMetadata(new Metadata());
|
||||
configMap.getMetadata().setName(configMapNameToUse);
|
||||
configMap.setData(data);
|
||||
client.create(configMap);
|
||||
return false;
|
||||
}
|
||||
|
||||
void startAction(String name) {
|
||||
stateTransition(name, currentState -> {
|
||||
boolean termination = false;
|
||||
switch (currentState) {
|
||||
case CREATED -> ensurePluginLoaded();
|
||||
case STARTED -> termination = true;
|
||||
// plugin can be started when it is stopped or failed
|
||||
case RESOLVED, STOPPED, FAILED -> doStart(name);
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
return termination;
|
||||
}, PluginState.STARTED);
|
||||
}
|
||||
|
||||
void stopAction(String name) {
|
||||
stateTransition(name, currentState -> {
|
||||
boolean termination = false;
|
||||
switch (currentState) {
|
||||
case CREATED -> ensurePluginLoaded();
|
||||
case RESOLVED, STARTED -> doStop(name);
|
||||
case FAILED, STOPPED -> termination = true;
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
return termination;
|
||||
}, PluginState.STOPPED);
|
||||
}
|
||||
|
||||
void stateTransition(String name, Function<PluginState, Boolean> stateAction,
|
||||
PluginState desiredState) {
|
||||
PluginWrapper pluginWrapper = getPluginWrapper(name);
|
||||
PluginState currentState = pluginWrapper.getPluginState();
|
||||
int maxRetries = PluginState.values().length;
|
||||
for (int i = 0; i < maxRetries && currentState != desiredState; i++) {
|
||||
try {
|
||||
if (log.isDebugEnabled() && i > 2) {
|
||||
log.debug("Plugin [{}] state transition from [{}] to [{}]", name, currentState,
|
||||
desiredState);
|
||||
}
|
||||
// When true is returned, the status loop is ended directly
|
||||
if (BooleanUtils.isTrue(stateAction.apply(currentState))) {
|
||||
break;
|
||||
}
|
||||
// update current state
|
||||
currentState = pluginWrapper.getPluginState();
|
||||
} catch (Throwable e) {
|
||||
persistenceFailureStatus(name, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void persistenceFailureStatus(String pluginName, Throwable e) {
|
||||
client.fetch(Plugin.class, pluginName).ifPresent(plugin -> {
|
||||
Plugin.PluginStatus status = plugin.statusNonNull();
|
||||
|
||||
PluginWrapper pluginWrapper = getPluginWrapper(pluginName);
|
||||
status.setPhase(pluginWrapper.getPluginState());
|
||||
|
||||
Plugin.PluginStatus oldStatus = JsonUtils.deepCopy(status);
|
||||
Condition condition = Condition.builder()
|
||||
.type(PluginState.FAILED.toString())
|
||||
.reason("UnexpectedState")
|
||||
.message(e.getMessage())
|
||||
.status(ConditionStatus.FALSE)
|
||||
.lastTransitionTime(Instant.now())
|
||||
.build();
|
||||
Plugin.PluginStatus.nullSafeConditions(status)
|
||||
.addAndEvictFIFO(condition);
|
||||
if (!Objects.equals(oldStatus, status)) {
|
||||
client.update(plugin);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private PluginWrapper getPluginWrapper(String name) {
|
||||
PluginWrapper pluginWrapper = haloPluginManager.getPlugin(name);
|
||||
if (pluginWrapper == null) {
|
||||
ensurePluginLoaded();
|
||||
pluginWrapper = haloPluginManager.getPlugin(name);
|
||||
}
|
||||
|
||||
if (pluginWrapper == null) {
|
||||
Plugin.PluginStatus status = new Plugin.PluginStatus();
|
||||
status.setPhase(PluginState.FAILED);
|
||||
|
||||
String errorMsg = "Plugin " + name + " not found in plugin manager.";
|
||||
Condition condition = Condition.builder()
|
||||
.type(PluginState.FAILED.toString())
|
||||
.reason("PluginNotFound")
|
||||
.message(errorMsg)
|
||||
.status(ConditionStatus.FALSE)
|
||||
.lastTransitionTime(Instant.now())
|
||||
.build();
|
||||
Plugin.PluginStatus.nullSafeConditions(status)
|
||||
.addAndEvictFIFO(condition);
|
||||
updateStatus(name, status);
|
||||
throw new PluginNotFoundException(errorMsg);
|
||||
}
|
||||
return pluginWrapper;
|
||||
}
|
||||
|
||||
void updateStatus(String name, Plugin.PluginStatus status) {
|
||||
if (status == null) {
|
||||
return;
|
||||
}
|
||||
client.fetch(Plugin.class, name).ifPresent(plugin -> {
|
||||
Plugin.PluginStatus oldStatus = JsonUtils.deepCopy(plugin.statusNonNull());
|
||||
plugin.setStatus(status);
|
||||
if (!Objects.equals(oldStatus, status)) {
|
||||
client.update(plugin);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void doStart(String name) {
|
||||
PluginWrapper pluginWrapper = getPluginWrapper(name);
|
||||
// Check if this plugin version is match requires param.
|
||||
if (!haloPluginManager.validatePluginVersion(pluginWrapper)) {
|
||||
PluginDescriptor descriptor = pluginWrapper.getDescriptor();
|
||||
String message = String.format(
|
||||
"Plugin requires a minimum system version of [%s], and you have [%s].",
|
||||
descriptor.getRequires(), haloPluginManager.getSystemVersion());
|
||||
throw new IllegalStateException(message);
|
||||
}
|
||||
|
||||
if (PluginState.DISABLED.equals(pluginWrapper.getPluginState())) {
|
||||
throw new IllegalStateException(
|
||||
"The plugin is disabled for some reason and cannot be started.");
|
||||
}
|
||||
|
||||
client.fetch(Plugin.class, name).ifPresent(plugin -> {
|
||||
final Plugin.PluginStatus status = plugin.statusNonNull();
|
||||
final Plugin.PluginStatus oldStatus = JsonUtils.deepCopy(status);
|
||||
|
||||
PluginState currentState = haloPluginManager.startPlugin(name);
|
||||
if (!PluginState.STARTED.equals(currentState)) {
|
||||
PluginStartingError staringErrorInfo = getStaringErrorInfo(name);
|
||||
log.debug("Failed to start plugin: " + staringErrorInfo.getDevMessage());
|
||||
throw new IllegalStateException(staringErrorInfo.getMessage());
|
||||
}
|
||||
|
||||
plugin.statusNonNull().setLastStartTime(Instant.now());
|
||||
|
||||
String jsBundlePath =
|
||||
BundleResourceUtils.getJsBundlePath(haloPluginManager, name);
|
||||
status.setEntry(jsBundlePath);
|
||||
|
||||
String cssBundlePath =
|
||||
BundleResourceUtils.getCssBundlePath(haloPluginManager, name);
|
||||
status.setStylesheet(cssBundlePath);
|
||||
|
||||
status.setPhase(currentState);
|
||||
Condition condition = Condition.builder()
|
||||
.type(PluginState.STARTED.toString())
|
||||
.reason(PluginState.STARTED.toString())
|
||||
.message("Started successfully")
|
||||
.lastTransitionTime(Instant.now())
|
||||
.status(ConditionStatus.TRUE)
|
||||
.build();
|
||||
Plugin.PluginStatus.nullSafeConditions(status)
|
||||
.addAndEvictFIFO(condition);
|
||||
if (!Objects.equals(oldStatus, status)) {
|
||||
client.update(plugin);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
PluginStartingError getStaringErrorInfo(String name) {
|
||||
PluginStartingError startingError =
|
||||
haloPluginManager.getPluginStartingError(name);
|
||||
if (startingError == null) {
|
||||
startingError = PluginStartingError.of(name, "Unknown error", "");
|
||||
}
|
||||
return startingError;
|
||||
}
|
||||
|
||||
void doStop(String name) {
|
||||
client.fetch(Plugin.class, name).ifPresent(plugin -> {
|
||||
final Plugin.PluginStatus status = plugin.statusNonNull();
|
||||
final Plugin.PluginStatus oldStatus = JsonUtils.deepCopy(status);
|
||||
|
||||
PluginState currentState = haloPluginManager.stopPlugin(name);
|
||||
if (!PluginState.STOPPED.equals(currentState)) {
|
||||
throw new IllegalStateException("Failed to stop plugin: " + name);
|
||||
}
|
||||
status.setPhase(currentState);
|
||||
// reset js bundle path
|
||||
status.setStylesheet(StringUtils.EMPTY);
|
||||
status.setEntry(StringUtils.EMPTY);
|
||||
|
||||
Condition condition = Condition.builder()
|
||||
.type(PluginState.STOPPED.toString())
|
||||
.reason(PluginState.STOPPED.toString())
|
||||
.message("Stopped successfully")
|
||||
.lastTransitionTime(Instant.now())
|
||||
.status(ConditionStatus.TRUE)
|
||||
.build();
|
||||
Plugin.PluginStatus.nullSafeConditions(status)
|
||||
.addAndEvictFIFO(condition);
|
||||
if (!Objects.equals(oldStatus, status)) {
|
||||
client.update(plugin);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -84,60 +448,16 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
ensurePluginLoaded();
|
||||
}
|
||||
|
||||
if (!checkPluginState(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.fetch(Plugin.class, name).ifPresent(plugin -> {
|
||||
Plugin oldPlugin = JsonUtils.deepCopy(plugin);
|
||||
Plugin.PluginStatus pluginStatus = plugin.statusNonNull();
|
||||
String logo = plugin.getSpec().getLogo();
|
||||
if (PathUtils.isAbsoluteUri(logo)) {
|
||||
pluginStatus.setLogo(logo);
|
||||
} else {
|
||||
String assetsPrefix =
|
||||
PluginConst.assertsRoutePrefix(plugin.getMetadata().getName());
|
||||
pluginStatus.setLogo(PathUtils.combinePath(assetsPrefix, logo));
|
||||
// Transition plugin status if necessary
|
||||
if (shouldReconcileStartState(plugin)) {
|
||||
startAction(name);
|
||||
}
|
||||
|
||||
if (!plugin.equals(oldPlugin)) {
|
||||
client.update(plugin);
|
||||
if (shouldReconcileStopState(plugin)) {
|
||||
stopAction(name);
|
||||
}
|
||||
});
|
||||
|
||||
startPlugin(name);
|
||||
|
||||
stopPlugin(name);
|
||||
}
|
||||
|
||||
private boolean checkPluginState(String name) {
|
||||
// check plugin state
|
||||
return client.fetch(Plugin.class, name)
|
||||
.map(plugin -> {
|
||||
Plugin oldPlugin = JsonUtils.deepCopy(plugin);
|
||||
Plugin.PluginStatus pluginStatus = plugin.statusNonNull();
|
||||
PluginWrapper pluginWrapper = haloPluginManager.getPlugin(name);
|
||||
if (pluginWrapper == null) {
|
||||
pluginStatus.setPhase(PluginState.FAILED);
|
||||
pluginStatus.setReason("PluginNotFound");
|
||||
pluginStatus.setMessage(
|
||||
"Plugin " + plugin.getMetadata().getName()
|
||||
+ " not found in plugin manager.");
|
||||
if (!plugin.equals(oldPlugin)) {
|
||||
client.update(plugin);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Set to the correct state
|
||||
pluginStatus.setPhase(pluginWrapper.getPluginState());
|
||||
|
||||
if (haloPluginManager.getUnresolvedPlugins().contains(pluginWrapper)) {
|
||||
// load and resolve plugin
|
||||
haloPluginManager.loadPlugin(pluginWrapper.getPluginPath());
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
private void ensurePluginLoaded() {
|
||||
|
@ -155,120 +475,15 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
}
|
||||
|
||||
private boolean shouldReconcileStartState(Plugin plugin) {
|
||||
return plugin.getSpec().getEnabled()
|
||||
&& plugin.statusNonNull().getPhase() != PluginState.STARTED;
|
||||
}
|
||||
|
||||
private void startPlugin(String pluginName) {
|
||||
client.fetch(Plugin.class, pluginName).ifPresent(plugin -> {
|
||||
final Plugin oldPlugin = JsonUtils.deepCopy(plugin);
|
||||
|
||||
// verify plugin meets the preconditions for startup
|
||||
if (!verifyStartCondition(pluginName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldReconcileStartState(plugin)) {
|
||||
PluginState currentState = haloPluginManager.startPlugin(pluginName);
|
||||
handleStatus(plugin, currentState, PluginState.STARTED);
|
||||
plugin.statusNonNull().setLastStartTime(Instant.now());
|
||||
}
|
||||
|
||||
settingDefaultConfig(plugin);
|
||||
|
||||
Plugin.PluginStatus status = plugin.statusNonNull();
|
||||
String jsBundlePath =
|
||||
BundleResourceUtils.getJsBundlePath(haloPluginManager, pluginName);
|
||||
status.setEntry(jsBundlePath);
|
||||
|
||||
String cssBundlePath =
|
||||
BundleResourceUtils.getCssBundlePath(haloPluginManager, pluginName);
|
||||
status.setStylesheet(cssBundlePath);
|
||||
|
||||
if (!plugin.equals(oldPlugin)) {
|
||||
client.update(plugin);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean verifyStartCondition(String pluginName) {
|
||||
PluginWrapper pluginWrapper = haloPluginManager.getPlugin(pluginName);
|
||||
if (pluginWrapper == null) {
|
||||
throw new PluginNotFoundException(
|
||||
"Plugin " + pluginName + " not found in plugin manager.");
|
||||
}
|
||||
return client.fetch(Plugin.class, pluginName).map(plugin -> {
|
||||
Plugin.PluginStatus oldStatus = JsonUtils.deepCopy(plugin.statusNonNull());
|
||||
|
||||
Plugin.PluginStatus status = plugin.statusNonNull();
|
||||
status.setLastTransitionTime(Instant.now());
|
||||
|
||||
// Check if this plugin version is match requires param.
|
||||
if (!haloPluginManager.validatePluginVersion(pluginWrapper)) {
|
||||
status.setPhase(PluginState.FAILED);
|
||||
status.setReason("PluginVersionNotMatch");
|
||||
String message = String.format(
|
||||
"Plugin requires a minimum system version of [%s], and you have [%s].",
|
||||
plugin.getSpec().getRequires(), haloPluginManager.getSystemVersion());
|
||||
status.setMessage(message);
|
||||
if (!oldStatus.equals(status)) {
|
||||
client.update(plugin);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
PluginState pluginState = pluginWrapper.getPluginState();
|
||||
if (PluginState.DISABLED.equals(pluginState)) {
|
||||
status.setPhase(pluginState);
|
||||
status.setReason("PluginDisabled");
|
||||
status.setMessage("The plugin is disabled for some reason and cannot be started.");
|
||||
if (!oldStatus.equals(status)) {
|
||||
client.update(plugin);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}).orElse(false);
|
||||
PluginWrapper pluginWrapper = getPluginWrapper(plugin.getMetadata().getName());
|
||||
return BooleanUtils.isTrue(plugin.getSpec().getEnabled())
|
||||
&& !PluginState.STARTED.equals(pluginWrapper.getPluginState());
|
||||
}
|
||||
|
||||
private boolean shouldReconcileStopState(Plugin plugin) {
|
||||
return !plugin.getSpec().getEnabled()
|
||||
&& plugin.statusNonNull().getPhase() == PluginState.STARTED;
|
||||
}
|
||||
|
||||
private void stopPlugin(String pluginName) {
|
||||
client.fetch(Plugin.class, pluginName).ifPresent(plugin -> {
|
||||
Plugin oldPlugin = JsonUtils.deepCopy(plugin);
|
||||
|
||||
if (shouldReconcileStopState(plugin)) {
|
||||
PluginState currentState = haloPluginManager.stopPlugin(pluginName);
|
||||
handleStatus(plugin, currentState, PluginState.STOPPED);
|
||||
}
|
||||
|
||||
if (!plugin.equals(oldPlugin)) {
|
||||
client.update(plugin);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleStatus(Plugin plugin, PluginState currentState,
|
||||
PluginState desiredState) {
|
||||
Plugin.PluginStatus status = plugin.statusNonNull();
|
||||
status.setPhase(currentState);
|
||||
status.setLastTransitionTime(Instant.now());
|
||||
if (desiredState.equals(currentState)) {
|
||||
plugin.getSpec().setEnabled(PluginState.STARTED.equals(currentState));
|
||||
} else {
|
||||
String pluginName = plugin.getMetadata().getName();
|
||||
PluginStartingError startingError =
|
||||
haloPluginManager.getPluginStartingError(plugin.getMetadata().getName());
|
||||
if (startingError == null) {
|
||||
startingError = PluginStartingError.of(pluginName, "Unknown error", "");
|
||||
}
|
||||
status.setReason(startingError.getMessage());
|
||||
status.setMessage(startingError.getDevMessage());
|
||||
client.fetch(Plugin.class, pluginName).ifPresent(client::update);
|
||||
throw new PluginRuntimeException(startingError.getMessage());
|
||||
}
|
||||
PluginWrapper pluginWrapper = getPluginWrapper(plugin.getMetadata().getName());
|
||||
return BooleanUtils.isFalse(plugin.getSpec().getEnabled())
|
||||
&& PluginState.STARTED.equals(pluginWrapper.getPluginState());
|
||||
}
|
||||
|
||||
private void addFinalizerIfNecessary(Plugin oldPlugin) {
|
||||
|
@ -302,16 +517,41 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
}
|
||||
|
||||
private void cleanUpResources(Plugin plugin) {
|
||||
PluginWrapper pluginWrapper = haloPluginManager.getPlugin(plugin.getMetadata().getName());
|
||||
String name = plugin.getMetadata().getName();
|
||||
// delete initial reverse proxy
|
||||
String initialReverseProxyName = initialReverseProxyName(name);
|
||||
client.fetch(ReverseProxy.class, initialReverseProxyName)
|
||||
.ifPresent(client::delete);
|
||||
retryTemplate.execute(callback -> {
|
||||
client.fetch(ReverseProxy.class, initialReverseProxyName).ifPresent(item -> {
|
||||
throw new IllegalStateException(
|
||||
"Waiting for reverseproxy [" + initialReverseProxyName + "] to be deleted.");
|
||||
});
|
||||
return null;
|
||||
});
|
||||
|
||||
// delete plugin setting form
|
||||
String settingName = plugin.getSpec().getSettingName();
|
||||
if (StringUtils.isNotBlank(settingName)) {
|
||||
client.fetch(Setting.class, settingName)
|
||||
.ifPresent(client::delete);
|
||||
retryTemplate.execute(callback -> {
|
||||
client.fetch(Setting.class, settingName).ifPresent(setting -> {
|
||||
throw new IllegalStateException("Waiting for setting to be deleted.");
|
||||
});
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
PluginWrapper pluginWrapper = haloPluginManager.getPlugin(name);
|
||||
if (pluginWrapper == null) {
|
||||
return;
|
||||
}
|
||||
// pluginWrapper must not be null in below code
|
||||
// stop and unload plugin, see also PluginBeforeStopSyncListener
|
||||
haloPluginManager.stopPlugin(pluginWrapper.getPluginId());
|
||||
haloPluginManager.unloadPlugin(pluginWrapper.getPluginId());
|
||||
// delete initial reverse proxy
|
||||
client.fetch(ReverseProxy.class, initialReverseProxyName(pluginWrapper.getPluginId()))
|
||||
.ifPresent(client::delete);
|
||||
if (!haloPluginManager.unloadPlugin(name)) {
|
||||
throw new IllegalStateException("Failed to unload plugin: " + name);
|
||||
}
|
||||
// delete plugin resources
|
||||
if (RuntimeMode.DEPLOYMENT.equals(pluginWrapper.getRuntimeMode())) {
|
||||
// delete plugin file
|
||||
|
@ -352,56 +592,6 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
}, () -> client.create(reverseProxy));
|
||||
}
|
||||
|
||||
private void settingDefaultConfig(Plugin plugin) {
|
||||
Assert.notNull(plugin, "The plugin must not be null.");
|
||||
final String settingName = plugin.getSpec().getSettingName();
|
||||
if (StringUtils.isBlank(settingName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String configMapNameToUse = plugin.getSpec().getConfigMapName();
|
||||
if (StringUtils.isBlank(configMapNameToUse)) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean existConfigMap = client.fetch(ConfigMap.class, configMapNameToUse)
|
||||
.isPresent();
|
||||
if (existConfigMap) {
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<Setting> settingOption = client.fetch(Setting.class, settingName);
|
||||
// Fix gh-3224
|
||||
// Maybe Setting is being created and cannot be queried. so try again.
|
||||
if (settingOption.isEmpty()) {
|
||||
client.fetch(Plugin.class, plugin.getMetadata().getName())
|
||||
.ifPresent(newPlugin -> {
|
||||
final Plugin.PluginStatus oldStatus =
|
||||
JsonUtils.deepCopy(newPlugin.statusNonNull());
|
||||
Plugin.PluginStatus status = newPlugin.statusNonNull();
|
||||
status.setPhase(PluginState.FAILED);
|
||||
status.setReason("ResourceNotReady");
|
||||
status.setMessage(
|
||||
"Setting named " + settingName + " is not ready, retrying...");
|
||||
status.setLastTransitionTime(Instant.now());
|
||||
if (!oldStatus.equals(status)
|
||||
&& !StringUtils.equals(oldStatus.getReason(), status.getReason())) {
|
||||
client.update(newPlugin);
|
||||
}
|
||||
throw new IllegalStateException(status.getMessage());
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var data = SettingUtils.settingDefinedDefaultValueMap(settingOption.get());
|
||||
// Create with or without default value
|
||||
ConfigMap configMap = new ConfigMap();
|
||||
configMap.setMetadata(new Metadata());
|
||||
configMap.getMetadata().setName(configMapNameToUse);
|
||||
configMap.setData(data);
|
||||
client.create(configMap);
|
||||
}
|
||||
|
||||
static String initialReverseProxyName(String pluginName) {
|
||||
return pluginName + "-system-generated-reverse-proxy";
|
||||
}
|
||||
|
|
|
@ -5,9 +5,13 @@ import java.time.Instant;
|
|||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* EqualsAndHashCode 排除了lastTransitionTime否则失败时,lastTransitionTime 会被更新
|
||||
* 导致 equals 为 false,一直被加入队列.
|
||||
*
|
||||
* @author guqing
|
||||
* @see
|
||||
* <a href="https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-conditions">pod-conditions</a>
|
||||
|
@ -17,6 +21,7 @@ import lombok.NoArgsConstructor;
|
|||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(exclude = "lastTransitionTime")
|
||||
public class Condition {
|
||||
/**
|
||||
* type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
|
|
|
@ -31,6 +31,9 @@ public class ConditionList extends AbstractCollection<Condition> {
|
|||
}
|
||||
|
||||
public boolean addFirst(@NonNull Condition condition) {
|
||||
if (isSame(conditions.peekFirst(), condition)) {
|
||||
return false;
|
||||
}
|
||||
conditions.addFirst(condition);
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.util.Map;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
@ -39,6 +40,14 @@ public class PluginBeforeStopSyncListener {
|
|||
return Flux.fromIterable(gvkExtensionNames.entrySet())
|
||||
.flatMap(entry -> Flux.fromIterable(entry.getValue())
|
||||
.flatMap(extensionName -> client.fetch(entry.getKey(), extensionName))
|
||||
.filter(unstructured -> {
|
||||
Map<String, String> annotations = unstructured.getMetadata().getAnnotations();
|
||||
if (annotations == null) {
|
||||
return true;
|
||||
}
|
||||
String stage = PluginConst.DeleteStage.STOP.name();
|
||||
return stage.equals(annotations.getOrDefault(PluginConst.DELETE_STAGE, stage));
|
||||
})
|
||||
.flatMap(client::delete))
|
||||
.then();
|
||||
}
|
||||
|
|
|
@ -12,9 +12,16 @@ public interface PluginConst {
|
|||
*/
|
||||
String PLUGIN_NAME_LABEL_NAME = "plugin.halo.run/plugin-name";
|
||||
|
||||
String DELETE_STAGE = "delete-stage";
|
||||
|
||||
String SYSTEM_PLUGIN_NAME = "system";
|
||||
|
||||
static String assertsRoutePrefix(String pluginName) {
|
||||
return "/plugins/" + pluginName + "/assets/";
|
||||
}
|
||||
|
||||
enum DeleteStage {
|
||||
STOP,
|
||||
UNINSTALL
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.pf4j.DevelopmentPluginClasspath;
|
||||
import org.pf4j.PluginRuntimeException;
|
||||
import org.pf4j.RuntimeMode;
|
||||
|
||||
@Slf4j
|
||||
public class PluginExtensionLoaderUtils {
|
||||
static final String EXTENSION_LOCATION = "extensions";
|
||||
static final DevelopmentPluginClasspath PLUGIN_CLASSPATH = new DevelopmentPluginClasspath();
|
||||
|
||||
public static Set<String> lookupExtensions(Path pluginPath, RuntimeMode runtimeMode) {
|
||||
if (RuntimeMode.DEVELOPMENT.equals(runtimeMode)) {
|
||||
return lookupFromClasses(pluginPath);
|
||||
} else {
|
||||
return lookupFromJar(pluginPath);
|
||||
}
|
||||
}
|
||||
|
||||
public static Set<String> lookupFromClasses(Path pluginPath) {
|
||||
Set<String> result = new HashSet<>();
|
||||
for (String directory : PLUGIN_CLASSPATH.getClassesDirectories()) {
|
||||
File file = pluginPath.resolve(directory).resolve(EXTENSION_LOCATION).toFile();
|
||||
if (file.exists() && file.isDirectory()) {
|
||||
result.addAll(walkExtensionFiles(file.toPath()));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Set<String> walkExtensionFiles(Path location) {
|
||||
try (Stream<Path> stream = Files.walk(location)) {
|
||||
return stream.map(Path::normalize)
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(path -> isYamlFile(path.getFileName().toString()))
|
||||
.map(path -> location.getParent().relativize(path).toString())
|
||||
.collect(Collectors.toSet());
|
||||
} catch (IOException e) {
|
||||
log.debug("Failed to walk extension files from [{}]", location);
|
||||
return Collections.emptySet();
|
||||
}
|
||||
}
|
||||
|
||||
static boolean isYamlFile(String path) {
|
||||
return path.endsWith(".yaml") || path.endsWith(".yml");
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Lists the path of the unstructured yaml configuration file from the plugin jar.</p>
|
||||
*
|
||||
* @param pluginJarPath plugin jar path
|
||||
* @return Unstructured file paths relative to plugin classpath
|
||||
* @throws PluginRuntimeException If loading the file fails
|
||||
*/
|
||||
static Set<String> lookupFromJar(Path pluginJarPath) {
|
||||
try (JarFile jarFile = new JarFile(pluginJarPath.toFile())) {
|
||||
return jarFile.stream()
|
||||
.filter(jarEntry -> {
|
||||
String name = jarEntry.getName();
|
||||
return name.startsWith(EXTENSION_LOCATION)
|
||||
&& !jarEntry.isDirectory()
|
||||
&& isYamlFile(name);
|
||||
})
|
||||
.map(ZipEntry::getName)
|
||||
.collect(Collectors.toSet());
|
||||
} catch (IOException e) {
|
||||
throw new PluginRuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +1,9 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import static run.halo.app.plugin.PluginExtensionLoaderUtils.lookupExtensions;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.pf4j.DevelopmentPluginClasspath;
|
||||
import org.pf4j.PluginRuntimeException;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.pf4j.RuntimeMode;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.Resource;
|
||||
|
@ -85,69 +72,4 @@ public class PluginStartedListener {
|
|||
}).then();
|
||||
}).then();
|
||||
}
|
||||
|
||||
Set<String> lookupExtensions(Path pluginPath, RuntimeMode runtimeMode) {
|
||||
if (RuntimeMode.DEVELOPMENT.equals(runtimeMode)) {
|
||||
return PluginExtensionLoaderUtils.lookupFromClasses(pluginPath);
|
||||
} else {
|
||||
return PluginExtensionLoaderUtils.lookupFromJar(pluginPath);
|
||||
}
|
||||
}
|
||||
|
||||
@Slf4j
|
||||
static class PluginExtensionLoaderUtils {
|
||||
static final String EXTENSION_LOCATION = "extensions";
|
||||
static final DevelopmentPluginClasspath PLUGIN_CLASSPATH = new DevelopmentPluginClasspath();
|
||||
|
||||
static Set<String> lookupFromClasses(Path pluginPath) {
|
||||
Set<String> result = new HashSet<>();
|
||||
for (String directory : PLUGIN_CLASSPATH.getClassesDirectories()) {
|
||||
File file = pluginPath.resolve(directory).resolve(EXTENSION_LOCATION).toFile();
|
||||
if (file.exists() && file.isDirectory()) {
|
||||
result.addAll(walkExtensionFiles(file.toPath()));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Set<String> walkExtensionFiles(Path location) {
|
||||
try (Stream<Path> stream = Files.walk(location)) {
|
||||
return stream.map(Path::normalize)
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(path -> isYamlFile(path.getFileName().toString()))
|
||||
.map(path -> location.getParent().relativize(path).toString())
|
||||
.collect(Collectors.toSet());
|
||||
} catch (IOException e) {
|
||||
log.debug("Failed to walk extension files from [{}]", location);
|
||||
return Collections.emptySet();
|
||||
}
|
||||
}
|
||||
|
||||
static boolean isYamlFile(String path) {
|
||||
return path.endsWith(".yaml") || path.endsWith(".yml");
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Lists the path of the unstructured yaml configuration file from the plugin jar.</p>
|
||||
*
|
||||
* @param pluginJarPath plugin jar path
|
||||
* @return Unstructured file paths relative to plugin classpath
|
||||
* @throws PluginRuntimeException If loading the file fails
|
||||
*/
|
||||
static Set<String> lookupFromJar(Path pluginJarPath) {
|
||||
try (JarFile jarFile = new JarFile(pluginJarPath.toFile())) {
|
||||
return jarFile.stream()
|
||||
.filter(jarEntry -> {
|
||||
String name = jarEntry.getName();
|
||||
return name.startsWith(EXTENSION_LOCATION)
|
||||
&& !jarEntry.isDirectory()
|
||||
&& isYamlFile(name);
|
||||
})
|
||||
.map(ZipEntry::getName)
|
||||
.collect(Collectors.toSet());
|
||||
} catch (IOException e) {
|
||||
throw new PluginRuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.pf4j.PluginRuntimeException;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.pf4j.PluginState;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.pf4j.RuntimeMode;
|
||||
|
@ -74,15 +74,18 @@ class PluginReconcilerTest {
|
|||
@DisplayName("Reconcile to start successfully")
|
||||
void reconcileOkWhenPluginManagerStartSuccessfully() {
|
||||
Plugin plugin = need2ReconcileForStartupState();
|
||||
when(extensionClient.fetch(eq(Plugin.class), eq("apples"))).thenReturn(Optional.of(plugin));
|
||||
when(haloPluginManager.startPlugin(any())).thenReturn(PluginState.STARTED);
|
||||
// mock plugin real state is started
|
||||
when(pluginWrapper.getPluginState()).thenReturn(PluginState.STOPPED);
|
||||
when(extensionClient.fetch(eq(Plugin.class), eq("apples"))).thenReturn(Optional.of(plugin));
|
||||
when(haloPluginManager.startPlugin(any())).thenAnswer((Answer<PluginState>) invocation -> {
|
||||
// mock plugin real state is started
|
||||
when(pluginWrapper.getPluginState()).thenReturn(PluginState.STARTED);
|
||||
return PluginState.STARTED;
|
||||
});
|
||||
|
||||
ArgumentCaptor<Plugin> pluginCaptor = doReconcileWithoutRequeue();
|
||||
verify(extensionClient, times(3)).update(isA(Plugin.class));
|
||||
verify(extensionClient, times(2)).update(isA(Plugin.class));
|
||||
|
||||
Plugin updateArgs = pluginCaptor.getValue();
|
||||
Plugin updateArgs = pluginCaptor.getAllValues().get(1);
|
||||
assertThat(updateArgs).isNotNull();
|
||||
assertThat(updateArgs.getSpec().getEnabled()).isTrue();
|
||||
assertThat(updateArgs.getStatus().getPhase()).isEqualTo(PluginState.STARTED);
|
||||
|
@ -93,10 +96,15 @@ class PluginReconcilerTest {
|
|||
@DisplayName("Reconcile to start failed")
|
||||
void reconcileOkWhenPluginManagerStartFailed() {
|
||||
Plugin plugin = need2ReconcileForStartupState();
|
||||
when(extensionClient.fetch(eq(Plugin.class), eq("apples"))).thenReturn(Optional.of(plugin));
|
||||
|
||||
// mock start plugin failed
|
||||
when(haloPluginManager.startPlugin(any())).thenReturn(PluginState.FAILED);
|
||||
when(extensionClient.fetch(eq(Plugin.class), eq("apples"))).thenReturn(Optional.of(plugin));
|
||||
when(haloPluginManager.startPlugin(any())).thenAnswer((Answer<PluginState>) invocation -> {
|
||||
// mock plugin real state is started
|
||||
when(pluginWrapper.getPluginState()).thenReturn(PluginState.FAILED);
|
||||
return PluginState.FAILED;
|
||||
});
|
||||
|
||||
// mock plugin real state is started
|
||||
when(pluginWrapper.getPluginState()).thenReturn(PluginState.STOPPED);
|
||||
|
||||
|
@ -114,10 +122,10 @@ class PluginReconcilerTest {
|
|||
|
||||
Plugin.PluginStatus status = updateArgs.getStatus();
|
||||
assertThat(status.getPhase()).isEqualTo(PluginState.FAILED);
|
||||
assertThat(status.getReason()).isEqualTo("error message");
|
||||
assertThat(status.getMessage()).isEqualTo("dev message");
|
||||
assertThat(status.getConditions().peek().getReason()).isEqualTo("error message");
|
||||
assertThat(status.getConditions().peek().getMessage()).isEqualTo("dev message");
|
||||
assertThat(status.getLastStartTime()).isNull();
|
||||
}).isInstanceOf(PluginRuntimeException.class)
|
||||
}).isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("error message");
|
||||
|
||||
}
|
||||
|
@ -127,12 +135,15 @@ class PluginReconcilerTest {
|
|||
void shouldReconcileStopWhenEnabledIsFalseAndPhaseIsStarted() {
|
||||
Plugin plugin = need2ReconcileForStopState();
|
||||
when(extensionClient.fetch(eq(Plugin.class), eq("apples"))).thenReturn(Optional.of(plugin));
|
||||
when(haloPluginManager.stopPlugin(any())).thenReturn(PluginState.STOPPED);
|
||||
when(haloPluginManager.stopPlugin(any())).thenAnswer((Answer<PluginState>) invocation -> {
|
||||
when(pluginWrapper.getPluginState()).thenReturn(PluginState.STOPPED);
|
||||
return PluginState.STOPPED;
|
||||
});
|
||||
// mock plugin real state is started
|
||||
when(pluginWrapper.getPluginState()).thenReturn(PluginState.STARTED);
|
||||
|
||||
ArgumentCaptor<Plugin> pluginCaptor = doReconcileWithoutRequeue();
|
||||
verify(extensionClient, times(4)).update(any(Plugin.class));
|
||||
verify(extensionClient, times(2)).update(any(Plugin.class));
|
||||
|
||||
Plugin updateArgs = pluginCaptor.getValue();
|
||||
assertThat(updateArgs).isNotNull();
|
||||
|
@ -162,12 +173,15 @@ class PluginReconcilerTest {
|
|||
}
|
||||
""", Plugin.class);
|
||||
when(extensionClient.fetch(eq(Plugin.class), eq("apples"))).thenReturn(Optional.of(plugin));
|
||||
when(haloPluginManager.stopPlugin(any())).thenReturn(PluginState.STOPPED);
|
||||
when(haloPluginManager.stopPlugin(any())).thenAnswer((Answer<PluginState>) invocation -> {
|
||||
when(pluginWrapper.getPluginState()).thenReturn(PluginState.STOPPED);
|
||||
return PluginState.STOPPED;
|
||||
});
|
||||
// mock plugin real state is started
|
||||
when(pluginWrapper.getPluginState()).thenReturn(PluginState.STARTED);
|
||||
|
||||
ArgumentCaptor<Plugin> pluginCaptor = doReconcileWithoutRequeue();
|
||||
verify(extensionClient, times(4)).update(any(Plugin.class));
|
||||
verify(extensionClient, times(2)).update(any(Plugin.class));
|
||||
|
||||
Plugin updateArgs = pluginCaptor.getValue();
|
||||
assertThat(updateArgs).isNotNull();
|
||||
|
@ -186,11 +200,6 @@ class PluginReconcilerTest {
|
|||
// mock plugin real state is started
|
||||
when(pluginWrapper.getPluginState()).thenReturn(PluginState.STARTED);
|
||||
|
||||
// mock stop failed message
|
||||
PluginStartingError pluginStartingError =
|
||||
PluginStartingError.of("apples", "error message", "dev message");
|
||||
when(haloPluginManager.getPluginStartingError(any())).thenReturn(pluginStartingError);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
ArgumentCaptor<Plugin> pluginCaptor = doReconcileNeedRequeue();
|
||||
|
||||
|
@ -200,10 +209,8 @@ class PluginReconcilerTest {
|
|||
|
||||
Plugin.PluginStatus status = updateArgs.getStatus();
|
||||
assertThat(status.getPhase()).isEqualTo(PluginState.FAILED);
|
||||
assertThat(status.getReason()).isEqualTo("error message");
|
||||
assertThat(status.getMessage()).isEqualTo("dev message");
|
||||
}).isInstanceOf(PluginRuntimeException.class)
|
||||
.hasMessage("error message");
|
||||
}).isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("Failed to stop plugin: apples");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -35,7 +35,7 @@ class PluginStartedListenerTest {
|
|||
Files.createFile(extensions.resolve("roles.yaml"));
|
||||
|
||||
Set<String> extensionResources =
|
||||
PluginStartedListener.PluginExtensionLoaderUtils.lookupFromClasses(tempPluginPath);
|
||||
PluginExtensionLoaderUtils.lookupFromClasses(tempPluginPath);
|
||||
assertThat(extensionResources)
|
||||
.containsAll(Set.of(Path.of("extensions/roles.yaml").toString()));
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ class PluginStartedListenerTest {
|
|||
Path targetJarPath = tempDirectory.resolve("plugin-0.0.1.jar");
|
||||
FileUtils.jar(Paths.get(plugin001Uri), targetJarPath);
|
||||
Set<String> unstructuredFilePathFromJar =
|
||||
PluginStartedListener.PluginExtensionLoaderUtils.lookupFromJar(targetJarPath);
|
||||
PluginExtensionLoaderUtils.lookupFromJar(targetJarPath);
|
||||
assertThat(unstructuredFilePathFromJar).hasSize(3);
|
||||
assertThat(unstructuredFilePathFromJar).containsAll(Set.of(
|
||||
Path.of("extensions/roles.yaml").toString(),
|
||||
|
|
Loading…
Reference in New Issue