mirror of https://github.com/halo-dev/halo
feat: add plugin reconciler (#2221)
* feat: add plugin reconciler * feat: add unit test casepull/2227/head
parent
3f7cb58370
commit
3e8f8b8789
|
@ -5,9 +5,11 @@ import org.springframework.context.annotation.Bean;
|
|||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import run.halo.app.core.extension.Plugin;
|
||||
import run.halo.app.core.extension.Role;
|
||||
import run.halo.app.core.extension.RoleBinding;
|
||||
import run.halo.app.core.extension.User;
|
||||
import run.halo.app.core.extension.reconciler.PluginReconciler;
|
||||
import run.halo.app.core.extension.reconciler.RoleBindingReconciler;
|
||||
import run.halo.app.core.extension.reconciler.RoleReconciler;
|
||||
import run.halo.app.core.extension.reconciler.UserReconciler;
|
||||
|
@ -23,6 +25,8 @@ import run.halo.app.extension.SchemeWatcherManager.SchemeWatcher;
|
|||
import run.halo.app.extension.controller.Controller;
|
||||
import run.halo.app.extension.controller.ControllerBuilder;
|
||||
import run.halo.app.extension.store.ExtensionStoreClient;
|
||||
import run.halo.app.plugin.HaloPluginManager;
|
||||
import run.halo.app.plugin.resources.JsBundleRuleProvider;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class ExtensionConfiguration {
|
||||
|
@ -72,4 +76,13 @@ public class ExtensionConfiguration {
|
|||
.extension(new RoleBinding())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Controller pluginController(ExtensionClient client, HaloPluginManager haloPluginManager,
|
||||
JsBundleRuleProvider jsBundleRule) {
|
||||
return new ControllerBuilder("plugin-controller", client)
|
||||
.reconciler(new PluginReconciler(client, haloPluginManager, jsBundleRule))
|
||||
.extension(new Plugin())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,172 @@
|
|||
package run.halo.app.core.extension.reconciler;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.pf4j.PluginRuntimeException;
|
||||
import org.pf4j.PluginState;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import run.halo.app.core.extension.Plugin;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.plugin.HaloPluginManager;
|
||||
import run.halo.app.plugin.PluginStartingError;
|
||||
import run.halo.app.plugin.resources.JsBundleRuleProvider;
|
||||
import run.halo.app.plugin.resources.ReverseProxyRouterFunctionFactory;
|
||||
|
||||
/**
|
||||
* Plugin reconciler.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class PluginReconciler implements Reconciler {
|
||||
|
||||
private final ExtensionClient client;
|
||||
private final HaloPluginManager haloPluginManager;
|
||||
|
||||
private final JsBundleRuleProvider jsBundleRule;
|
||||
|
||||
public PluginReconciler(ExtensionClient client,
|
||||
HaloPluginManager haloPluginManager,
|
||||
JsBundleRuleProvider jsBundleRule) {
|
||||
this.client = client;
|
||||
this.haloPluginManager = haloPluginManager;
|
||||
this.jsBundleRule = jsBundleRule;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
return client.fetch(Plugin.class, request.name())
|
||||
.map(plugin -> {
|
||||
final Plugin oldPlugin = deepCopy(plugin);
|
||||
try {
|
||||
reconcilePluginState(plugin);
|
||||
// TODO: reconcile other plugin resources
|
||||
|
||||
if (!Objects.equals(oldPlugin, plugin)) {
|
||||
// update plugin when attributes changed
|
||||
client.update(plugin);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// update plugin and requeue
|
||||
client.update(plugin);
|
||||
log.error(e.getMessage(), e);
|
||||
return new Result(true, null);
|
||||
}
|
||||
return new Result(false, null);
|
||||
})
|
||||
.orElse(new Result(false, null));
|
||||
}
|
||||
|
||||
private void reconcilePluginState(Plugin plugin) {
|
||||
Plugin.PluginStatus pluginStatus = plugin.getStatus();
|
||||
String name = plugin.getMetadata().getName();
|
||||
PluginWrapper pluginWrapper = haloPluginManager.getPlugin(name);
|
||||
if (pluginWrapper == null) {
|
||||
ensurePluginLoaded();
|
||||
pluginWrapper = haloPluginManager.getPlugin(name);
|
||||
}
|
||||
|
||||
if (pluginWrapper == null) {
|
||||
pluginStatus.setPhase(PluginState.FAILED);
|
||||
pluginStatus.setReason("PluginNotFound");
|
||||
pluginStatus.setMessage("Plugin " + name + " not found in plugin manager");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Objects.equals(pluginStatus.getPhase(), pluginWrapper.getPluginState())) {
|
||||
// Set to the correct state
|
||||
pluginStatus.setPhase(pluginWrapper.getPluginState());
|
||||
}
|
||||
|
||||
if (haloPluginManager.getUnresolvedPlugins().contains(pluginWrapper)) {
|
||||
// load and resolve plugin
|
||||
haloPluginManager.loadPlugin(pluginWrapper.getPluginPath());
|
||||
}
|
||||
|
||||
if (shouldReconcileStartState(plugin)) {
|
||||
startPlugin(plugin);
|
||||
}
|
||||
|
||||
if (shouldReconcileStopState(plugin)) {
|
||||
stopPlugin(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
private Plugin deepCopy(Plugin plugin) {
|
||||
return JsonUtils.jsonToObject(JsonUtils.objectToJson(plugin), Plugin.class);
|
||||
}
|
||||
|
||||
private void ensurePluginLoaded() {
|
||||
// load plugin if exists in plugin root paths.
|
||||
List<PluginWrapper> loadedPlugins = haloPluginManager.getPlugins();
|
||||
Map<Path, PluginWrapper> loadedPluginWrapperMap = loadedPlugins.stream()
|
||||
.collect(Collectors.toMap(PluginWrapper::getPluginPath, item -> item));
|
||||
haloPluginManager.getPluginRepository()
|
||||
.getPluginPaths()
|
||||
.forEach(path -> {
|
||||
if (!loadedPluginWrapperMap.containsKey(path)) {
|
||||
haloPluginManager.loadPlugin(path);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean shouldReconcileStartState(Plugin plugin) {
|
||||
return plugin.getSpec().getEnabled()
|
||||
&& plugin.getStatus().getPhase() != PluginState.STARTED;
|
||||
}
|
||||
|
||||
private void startPlugin(Plugin plugin) {
|
||||
String pluginName = plugin.getMetadata().getName();
|
||||
PluginState currentState = haloPluginManager.startPlugin(pluginName);
|
||||
handleStatus(plugin, currentState, PluginState.STARTED);
|
||||
Plugin.PluginStatus status = plugin.getStatus();
|
||||
// TODO Check whether the JS bundle rule exists. If it does not exist, do not populate
|
||||
// populate stylesheet path
|
||||
String jsBundleRoute = ReverseProxyRouterFunctionFactory.buildRoutePath(pluginName,
|
||||
jsBundleRule.jsRule(pluginName));
|
||||
String cssBundleRoute = ReverseProxyRouterFunctionFactory.buildRoutePath(pluginName,
|
||||
jsBundleRule.cssRule(pluginName));
|
||||
status.setEntry(jsBundleRoute);
|
||||
status.setStylesheet(cssBundleRoute);
|
||||
status.setLastStartTime(Instant.now());
|
||||
}
|
||||
|
||||
private boolean shouldReconcileStopState(Plugin plugin) {
|
||||
return !plugin.getSpec().getEnabled()
|
||||
&& plugin.getStatus().getPhase() == PluginState.STARTED;
|
||||
}
|
||||
|
||||
private void stopPlugin(Plugin plugin) {
|
||||
String pluginName = plugin.getMetadata().getName();
|
||||
PluginState currentState = haloPluginManager.stopPlugin(pluginName);
|
||||
handleStatus(plugin, currentState, PluginState.STOPPED);
|
||||
}
|
||||
|
||||
private void handleStatus(Plugin plugin, PluginState currentState,
|
||||
PluginState desiredState) {
|
||||
Plugin.PluginStatus status = plugin.getStatus();
|
||||
if (status == null) {
|
||||
status = new Plugin.PluginStatus();
|
||||
}
|
||||
status.setPhase(currentState);
|
||||
status.setLastTransitionTime(Instant.now());
|
||||
if (desiredState.equals(currentState)) {
|
||||
plugin.getSpec().setEnabled(PluginState.STARTED.equals(currentState));
|
||||
} else {
|
||||
PluginStartingError startingError =
|
||||
haloPluginManager.getPluginStartingError(plugin.getMetadata().getName());
|
||||
status.setReason(startingError.getMessage());
|
||||
status.setMessage(startingError.getDevMessage());
|
||||
// requeue the plugin for reconciliation
|
||||
throw new PluginRuntimeException(startingError.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.core.extension.Plugin;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
|
||||
/**
|
||||
* Load plugins after application ready.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class PluginInitializationLoadOnApplicationReady
|
||||
implements ApplicationListener<ApplicationReadyEvent> {
|
||||
|
||||
private final PluginService pluginService;
|
||||
|
||||
private final HaloPluginManager haloPluginManager;
|
||||
|
||||
private final ExtensionClient extensionClient;
|
||||
|
||||
public PluginInitializationLoadOnApplicationReady(PluginService pluginService,
|
||||
HaloPluginManager haloPluginManager, ExtensionClient extensionClient) {
|
||||
this.pluginService = pluginService;
|
||||
this.haloPluginManager = haloPluginManager;
|
||||
this.extensionClient = extensionClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(@NonNull ApplicationReadyEvent event) {
|
||||
haloPluginManager.loadPlugins();
|
||||
initStartupPlugins();
|
||||
}
|
||||
|
||||
private void initStartupPlugins() {
|
||||
extensionClient.list(Plugin.class,
|
||||
predicate -> predicate.getSpec().getEnabled(),
|
||||
null)
|
||||
.forEach(plugin -> pluginService.startup(plugin.getMetadata().getName()));
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import run.halo.app.core.extension.Plugin;
|
||||
|
||||
/**
|
||||
* Plugin manager controller.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
// TODO Optimize prefix configuration
|
||||
@RequestMapping("/apis/plugin.halo.run/v1alpha1/plugins")
|
||||
public class PluginLifeCycleManagerController {
|
||||
|
||||
private final PluginService pluginService;
|
||||
|
||||
public PluginLifeCycleManagerController(PluginService pluginService) {
|
||||
this.pluginService = pluginService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<Plugin> list() {
|
||||
return pluginService.list();
|
||||
}
|
||||
|
||||
@PutMapping("/{pluginName}/startup")
|
||||
public Plugin start(@PathVariable String pluginName) {
|
||||
return pluginService.startup(pluginName);
|
||||
}
|
||||
|
||||
@PutMapping("/{pluginName}/stop")
|
||||
public Plugin stop(@PathVariable String pluginName) {
|
||||
return pluginService.stop(pluginName);
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.util.List;
|
||||
import run.halo.app.core.extension.Plugin;
|
||||
|
||||
/**
|
||||
* Service for plugin.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public interface PluginService {
|
||||
|
||||
/**
|
||||
* Lists plugin from repository.
|
||||
*
|
||||
* @return all loaded plugins.
|
||||
*/
|
||||
List<Plugin> list();
|
||||
|
||||
/**
|
||||
* Start the plugin according to the plugin name.
|
||||
*
|
||||
* @param pluginName plugin name
|
||||
* @return plugin custom resource
|
||||
*/
|
||||
Plugin startup(String pluginName);
|
||||
|
||||
Plugin stop(String pluginName);
|
||||
|
||||
/**
|
||||
* Gets {@link Plugin} by plugin name.
|
||||
*
|
||||
* @param pluginName plugin name
|
||||
* @return plugin custom resource
|
||||
*/
|
||||
Plugin getByName(String pluginName);
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import org.pf4j.PluginState;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
import run.halo.app.core.extension.Plugin;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.plugin.resources.JsBundleRuleProvider;
|
||||
import run.halo.app.plugin.resources.ReverseProxyRouterFunctionFactory;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link PluginService}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Service
|
||||
public class PluginServiceImpl implements PluginService {
|
||||
|
||||
private final ExtensionClient extensionClient;
|
||||
|
||||
private final HaloPluginManager haloPluginManager;
|
||||
|
||||
private final JsBundleRuleProvider jsBundleRule;
|
||||
|
||||
public PluginServiceImpl(ExtensionClient extensionClient,
|
||||
HaloPluginManager haloPluginManager, JsBundleRuleProvider jsBundleRule) {
|
||||
this.extensionClient = extensionClient;
|
||||
this.haloPluginManager = haloPluginManager;
|
||||
this.jsBundleRule = jsBundleRule;
|
||||
}
|
||||
|
||||
/**
|
||||
* list all plugins including loaded and unloaded.
|
||||
*
|
||||
* @return plugin info
|
||||
*/
|
||||
public List<Plugin> list() {
|
||||
return extensionClient.list(Plugin.class, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Plugin startup(String pluginName) {
|
||||
Assert.notNull(pluginName, "The pluginName must not be null.");
|
||||
PluginState currentState = haloPluginManager.startPlugin(pluginName);
|
||||
|
||||
Plugin plugin = handleStatus(pluginName, currentState, PluginState.STARTED);
|
||||
Plugin.PluginStatus status = plugin.getStatus();
|
||||
// TODO Check whether the JS bundle rule exists. If it does not exist, do not populate
|
||||
// populate stylesheet path
|
||||
String jsBundleRoute = ReverseProxyRouterFunctionFactory.buildRoutePath(pluginName,
|
||||
jsBundleRule.jsRule(pluginName));
|
||||
String cssBundleRoute = ReverseProxyRouterFunctionFactory.buildRoutePath(pluginName,
|
||||
jsBundleRule.cssRule(pluginName));
|
||||
status.setEntry(jsBundleRoute);
|
||||
status.setStylesheet(cssBundleRoute);
|
||||
status.setLastStartTime(Instant.now());
|
||||
extensionClient.update(plugin);
|
||||
return plugin;
|
||||
}
|
||||
|
||||
private Plugin handleStatus(String pluginName, PluginState currentState,
|
||||
PluginState desiredState) {
|
||||
Plugin plugin = getByName(pluginName);
|
||||
Plugin.PluginStatus status = plugin.getStatus();
|
||||
if (status == null) {
|
||||
status = new Plugin.PluginStatus();
|
||||
}
|
||||
status.setPhase(currentState);
|
||||
if (desiredState.equals(currentState)) {
|
||||
plugin.getSpec().setEnabled(true);
|
||||
} else {
|
||||
PluginStartingError startingError =
|
||||
haloPluginManager.getPluginStartingError(pluginName);
|
||||
status.setReason(startingError.getMessage());
|
||||
status.setMessage(startingError.getDevMessage());
|
||||
}
|
||||
return plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Plugin stop(String pluginName) {
|
||||
Assert.notNull(pluginName, "The pluginName must not be null.");
|
||||
PluginState currentState = haloPluginManager.stopPlugin(pluginName);
|
||||
Plugin plugin = handleStatus(pluginName, currentState, PluginState.STOPPED);
|
||||
extensionClient.update(plugin);
|
||||
return plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Plugin getByName(String pluginName) {
|
||||
Assert.notNull(pluginName, "The pluginName must not be null.");
|
||||
return extensionClient.fetch(Plugin.class, pluginName)
|
||||
.orElseThrow(() ->
|
||||
new PluginNotFoundException(String.format("Plugin [%s] not found", pluginName)));
|
||||
}
|
||||
}
|
|
@ -60,7 +60,8 @@ public class DefaultRuleResolver implements AuthorizationRuleResolver {
|
|||
rules = fetchRules(role);
|
||||
} catch (Exception e) {
|
||||
if (visitor.visit(null, null, e)) {
|
||||
return;
|
||||
// if visitor returns true, we continue visiting
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,248 @@
|
|||
package run.halo.app.core.extension.reconciler;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.pf4j.PluginState;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import run.halo.app.core.extension.Plugin;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.plugin.HaloPluginManager;
|
||||
import run.halo.app.plugin.PluginStartingError;
|
||||
import run.halo.app.plugin.resources.JsBundleRuleProvider;
|
||||
|
||||
/**
|
||||
* Tests for {@link PluginReconciler}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PluginReconcilerTest {
|
||||
|
||||
@Mock
|
||||
HaloPluginManager haloPluginManager;
|
||||
|
||||
@Mock
|
||||
ExtensionClient extensionClient;
|
||||
|
||||
@Mock
|
||||
PluginWrapper pluginWrapper;
|
||||
|
||||
PluginReconciler pluginReconciler;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
JsBundleRuleProvider jsBundleRule = new JsBundleRuleProvider();
|
||||
pluginReconciler = new PluginReconciler(extensionClient, haloPluginManager, jsBundleRule);
|
||||
|
||||
when(haloPluginManager.getPlugin(any())).thenReturn(pluginWrapper);
|
||||
when(haloPluginManager.getUnresolvedPlugins()).thenReturn(List.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
@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);
|
||||
|
||||
ArgumentCaptor<Plugin> pluginCaptor = doReconcileWithoutRequeue();
|
||||
|
||||
Plugin updateArgs = pluginCaptor.getValue();
|
||||
assertThat(updateArgs).isNotNull();
|
||||
assertThat(updateArgs.getSpec().getEnabled()).isTrue();
|
||||
assertThat(updateArgs.getStatus().getPhase()).isEqualTo(PluginState.STARTED);
|
||||
assertThat(updateArgs.getStatus().getLastStartTime()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@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);
|
||||
// mock plugin real state is started
|
||||
when(pluginWrapper.getPluginState()).thenReturn(PluginState.STOPPED);
|
||||
|
||||
PluginStartingError pluginStartingError =
|
||||
PluginStartingError.of("apples", "error message", "dev message");
|
||||
when(haloPluginManager.getPluginStartingError(any())).thenReturn(pluginStartingError);
|
||||
|
||||
ArgumentCaptor<Plugin> pluginCaptor = doReconcileNeedRequeue();
|
||||
|
||||
// Verify the state before the update plugin
|
||||
Plugin updateArgs = pluginCaptor.getValue();
|
||||
assertThat(updateArgs).isNotNull();
|
||||
assertThat(updateArgs.getSpec().getEnabled()).isTrue();
|
||||
|
||||
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.getLastStartTime()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Reconcile to stop successfully")
|
||||
void shouldReconcileStopWhenEnabledIsFalseAndPhaseIsStarted() {
|
||||
Plugin plugin = need2ReconcileForStopState();
|
||||
when(extensionClient.fetch(eq(Plugin.class), eq("apples"))).thenReturn(Optional.of(plugin));
|
||||
when(haloPluginManager.stopPlugin(any())).thenReturn(PluginState.STOPPED);
|
||||
// mock plugin real state is started
|
||||
when(pluginWrapper.getPluginState()).thenReturn(PluginState.STARTED);
|
||||
|
||||
ArgumentCaptor<Plugin> pluginCaptor = doReconcileWithoutRequeue();
|
||||
|
||||
Plugin updateArgs = pluginCaptor.getValue();
|
||||
assertThat(updateArgs).isNotNull();
|
||||
assertThat(updateArgs.getSpec().getEnabled()).isFalse();
|
||||
assertThat(updateArgs.getStatus().getPhase()).isEqualTo(PluginState.STOPPED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Reconcile to stop successfully when 'spec.enabled' is inconsistent"
|
||||
+ " with 'status.phase'")
|
||||
void shouldReconcileStopWhenEnabledIsFalseAndPhaseIsStopped() {
|
||||
// 模拟插件的实际状态与status.phase记录的状态不一致
|
||||
Plugin plugin = JsonUtils.jsonToObject("""
|
||||
{
|
||||
"apiVersion": "plugin.halo.run/v1alpha1",
|
||||
"kind": "Plugin",
|
||||
"metadata": {
|
||||
"name": "apples"
|
||||
},
|
||||
"spec": {
|
||||
"displayName": "测试插件",
|
||||
"enabled": false
|
||||
},
|
||||
"status": {
|
||||
"phase": "STOPPED"
|
||||
}
|
||||
}
|
||||
""", Plugin.class);
|
||||
when(extensionClient.fetch(eq(Plugin.class), eq("apples"))).thenReturn(Optional.of(plugin));
|
||||
when(haloPluginManager.stopPlugin(any())).thenReturn(PluginState.STOPPED);
|
||||
// mock plugin real state is started
|
||||
when(pluginWrapper.getPluginState()).thenReturn(PluginState.STARTED);
|
||||
|
||||
ArgumentCaptor<Plugin> pluginCaptor = doReconcileWithoutRequeue();
|
||||
|
||||
Plugin updateArgs = pluginCaptor.getValue();
|
||||
assertThat(updateArgs).isNotNull();
|
||||
assertThat(updateArgs.getSpec().getEnabled()).isFalse();
|
||||
assertThat(updateArgs.getStatus().getPhase()).isEqualTo(PluginState.STOPPED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Reconcile to stop failed")
|
||||
void shouldReconcileStopWhenEnabledIsFalseAndPhaseIsStartedButStopFailed() {
|
||||
Plugin plugin = need2ReconcileForStopState();
|
||||
when(extensionClient.fetch(eq(Plugin.class), eq("apples"))).thenReturn(Optional.of(plugin));
|
||||
// mock stop failed
|
||||
when(haloPluginManager.stopPlugin(any())).thenReturn(PluginState.FAILED);
|
||||
|
||||
// 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);
|
||||
|
||||
ArgumentCaptor<Plugin> pluginCaptor = doReconcileNeedRequeue();
|
||||
|
||||
Plugin updateArgs = pluginCaptor.getValue();
|
||||
assertThat(updateArgs).isNotNull();
|
||||
assertThat(updateArgs.getSpec().getEnabled()).isFalse();
|
||||
|
||||
Plugin.PluginStatus status = updateArgs.getStatus();
|
||||
assertThat(status.getPhase()).isEqualTo(PluginState.FAILED);
|
||||
assertThat(status.getReason()).isEqualTo("error message");
|
||||
assertThat(status.getMessage()).isEqualTo("dev message");
|
||||
}
|
||||
|
||||
private ArgumentCaptor<Plugin> doReconcileNeedRequeue() {
|
||||
ArgumentCaptor<Plugin> pluginCaptor = ArgumentCaptor.forClass(Plugin.class);
|
||||
doNothing().when(extensionClient).update(pluginCaptor.capture());
|
||||
|
||||
// reconcile
|
||||
Reconciler.Result result = pluginReconciler.reconcile(new Reconciler.Request("apples"));
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.reEnqueue()).isEqualTo(true);
|
||||
|
||||
verify(extensionClient, times(1)).update(any());
|
||||
return pluginCaptor;
|
||||
}
|
||||
|
||||
private ArgumentCaptor<Plugin> doReconcileWithoutRequeue() {
|
||||
ArgumentCaptor<Plugin> pluginCaptor = ArgumentCaptor.forClass(Plugin.class);
|
||||
doNothing().when(extensionClient).update(pluginCaptor.capture());
|
||||
|
||||
// reconcile
|
||||
Reconciler.Result result = pluginReconciler.reconcile(new Reconciler.Request("apples"));
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.reEnqueue()).isEqualTo(false);
|
||||
|
||||
verify(extensionClient, times(1)).update(any());
|
||||
return pluginCaptor;
|
||||
}
|
||||
|
||||
private Plugin need2ReconcileForStartupState() {
|
||||
return JsonUtils.jsonToObject("""
|
||||
{
|
||||
"apiVersion": "plugin.halo.run/v1alpha1",
|
||||
"kind": "Plugin",
|
||||
"metadata": {
|
||||
"name": "apples"
|
||||
},
|
||||
"spec": {
|
||||
"displayName": "测试插件",
|
||||
"enabled": true
|
||||
},
|
||||
"status": {
|
||||
"phase": "STOPPED"
|
||||
}
|
||||
}
|
||||
""", Plugin.class);
|
||||
}
|
||||
|
||||
private Plugin need2ReconcileForStopState() {
|
||||
return JsonUtils.jsonToObject("""
|
||||
{
|
||||
"apiVersion": "plugin.halo.run/v1alpha1",
|
||||
"kind": "Plugin",
|
||||
"metadata": {
|
||||
"name": "apples"
|
||||
},
|
||||
"spec": {
|
||||
"displayName": "测试插件",
|
||||
"enabled": false
|
||||
},
|
||||
"status": {
|
||||
"phase": "STARTED"
|
||||
}
|
||||
}
|
||||
""", Plugin.class);
|
||||
}
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.pf4j.PluginState;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import run.halo.app.core.extension.Role;
|
||||
import run.halo.app.core.extension.service.RoleService;
|
||||
import run.halo.app.extension.Metadata;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@SpringBootTest
|
||||
@WithMockUser(username = "user")
|
||||
@AutoConfigureWebTestClient
|
||||
class PluginLifeCycleManagerControllerTest {
|
||||
|
||||
@Autowired
|
||||
WebTestClient webClient;
|
||||
|
||||
@MockBean
|
||||
RoleService roleService;
|
||||
|
||||
@MockBean
|
||||
HaloPluginManager haloPluginManager;
|
||||
|
||||
String prefix = "/apis/plugin.halo.run/v1alpha1/plugins";
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// There is no default role available in the system, so it needs to be created
|
||||
Role role = new Role();
|
||||
role.setApiVersion("v1alpha1");
|
||||
role.setKind("Role");
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName("test-plugin-lifecycle-role");
|
||||
role.setMetadata(metadata);
|
||||
Role.PolicyRule policyRule = new Role.PolicyRule.Builder()
|
||||
.apiGroups("plugin.halo.run")
|
||||
.resources("plugins", "plugins/startup", "plugins/stop")
|
||||
.verbs("*")
|
||||
.build();
|
||||
role.setRules(List.of(policyRule));
|
||||
when(roleService.getRole("USER")).thenReturn(role);
|
||||
when(haloPluginManager.startPlugin(any())).thenReturn(PluginState.STARTED);
|
||||
when(haloPluginManager.stopPlugin(any())).thenReturn(PluginState.STOPPED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void list() {
|
||||
webClient.get()
|
||||
.uri(prefix)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void start() {
|
||||
webClient.put()
|
||||
.uri(prefix + "/apples/startup")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.is5xxServerError();
|
||||
}
|
||||
|
||||
@Test
|
||||
void stop() {
|
||||
webClient.put()
|
||||
.uri(prefix + "/apples/stop")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.is5xxServerError();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue