feat: add plugin reconciler (#2221)

* feat: add plugin reconciler

* feat: add unit test case
pull/2227/head
guqing 2022-07-08 15:08:13 +08:00 committed by GitHub
parent 3f7cb58370
commit 3e8f8b8789
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 435 additions and 313 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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