feat: add plugin status manage (#2177)

* feat: add plugin status manage

* feat: add plugin state changed listener

* refactor: plugin status

* refactor: plugin
pull/2190/head
guqing 2022-06-23 11:08:24 +08:00 committed by GitHub
parent c24df6fb05
commit 273ffaad48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 283 additions and 58 deletions

View File

@ -1,7 +1,8 @@
package run.halo.app.plugin;
package run.halo.app.core.extension;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -10,8 +11,10 @@ import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.pf4j.PluginState;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
import run.halo.app.plugin.BasePlugin;
/**
* A custom resource for Plugin.
@ -29,6 +32,8 @@ public class Plugin extends AbstractExtension {
@Schema(required = true)
private PluginSpec spec;
private PluginStatus status;
@Data
public static class PluginSpec {
@ -55,6 +60,8 @@ public class Plugin extends AbstractExtension {
private String requires = "*";
private String pluginClass = BasePlugin.class.getName();
private Boolean enabled = false;
}
@Getter
@ -71,4 +78,22 @@ public class Plugin extends AbstractExtension {
this.url = "";
}
}
@Data
public static class PluginStatus {
private PluginState phase;
private String reason;
private String message;
private Instant lastStartTime;
private Instant lastTransitionTime;
private String entry;
private String stylesheet;
}
}

View File

@ -1,4 +1,4 @@
package run.halo.app.plugin.resources;
package run.halo.app.core.extension;
import java.util.List;
import lombok.Data;
@ -21,9 +21,9 @@ import run.halo.app.extension.GVK;
public class ReverseProxy extends AbstractExtension {
private List<ReverseProxyRule> rules;
record ReverseProxyRule(String path, FileReverseProxyProvider file) {
public record ReverseProxyRule(String path, FileReverseProxyProvider file) {
}
record FileReverseProxyProvider(String directory, String filename) {
public record FileReverseProxyProvider(String directory, String filename) {
}
}

View File

@ -4,11 +4,12 @@ import org.springframework.boot.context.event.ApplicationStartedEvent;
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.core.extension.ReverseProxy;
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.extension.SchemeManager;
import run.halo.app.plugin.Plugin;
import run.halo.app.security.authentication.pat.PersonalAccessToken;
@Component
@ -27,5 +28,6 @@ public class SchemeInitializer implements ApplicationListener<ApplicationStarted
schemeManager.register(Plugin.class);
schemeManager.register(RoleBinding.class);
schemeManager.register(User.class);
schemeManager.register(ReverseProxy.class);
}
}

View File

@ -2,7 +2,10 @@ 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.
@ -14,14 +17,29 @@ import org.springframework.stereotype.Component;
public class PluginInitializationLoadOnApplicationReady
implements ApplicationListener<ApplicationReadyEvent> {
private final PluginService pluginService;
private final HaloPluginManager haloPluginManager;
public PluginInitializationLoadOnApplicationReady(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(ApplicationReadyEvent event) {
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

@ -2,11 +2,12 @@ package run.halo.app.plugin;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.pf4j.PluginState;
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.
@ -20,13 +21,10 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/apis/plugin.halo.run/v1alpha1/plugins")
public class PluginLifeCycleManagerController {
private final PluginServiceImpl pluginService;
private final HaloPluginManager pluginManager;
private final PluginService pluginService;
public PluginLifeCycleManagerController(PluginServiceImpl pluginService,
HaloPluginManager pluginManager) {
public PluginLifeCycleManagerController(PluginService pluginService) {
this.pluginService = pluginService;
this.pluginManager = pluginManager;
}
@GetMapping
@ -34,13 +32,13 @@ public class PluginLifeCycleManagerController {
return pluginService.list();
}
@GetMapping("/{pluginName}/startup")
public PluginState start(@PathVariable String pluginName) {
return pluginManager.startPlugin(pluginName);
@PutMapping("/{pluginName}/startup")
public Plugin start(@PathVariable String pluginName) {
return pluginService.startup(pluginName);
}
@GetMapping("/{pluginName}/stop")
public PluginState stop(@PathVariable String pluginName) {
return pluginManager.stopPlugin(pluginName);
@PutMapping("/{pluginName}/stop")
public Plugin stop(@PathVariable String pluginName) {
return pluginService.stop(pluginName);
}
}

View File

@ -2,14 +2,10 @@ package run.halo.app.plugin;
import org.pf4j.PluginWrapper;
import org.springframework.context.ApplicationListener;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import run.halo.app.core.extension.Plugin;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.SchemeManager;
import run.halo.app.infra.utils.YamlUnstructuredLoader;
import run.halo.app.plugin.event.HaloPluginLoadedEvent;
import run.halo.app.plugin.resources.ReverseProxy;
/**
* @author guqing
@ -17,15 +13,13 @@ import run.halo.app.plugin.resources.ReverseProxy;
*/
@Component
public class PluginLoadedListener implements ApplicationListener<HaloPluginLoadedEvent> {
private static final String REVERSE_PROXY_NAME = "extensions/reverseProxy.yaml";
private final ExtensionClient extensionClient;
public PluginLoadedListener(ExtensionClient extensionClient, SchemeManager schemeManager) {
this.extensionClient = extensionClient;
private final PluginUnstructuredResourceLoader pluginUnstructuredResourceLoader;
// TODO Optimize schemes register
schemeManager.register(Plugin.class);
schemeManager.register(ReverseProxy.class);
public PluginLoadedListener(ExtensionClient extensionClient) {
this.extensionClient = extensionClient;
pluginUnstructuredResourceLoader = new PluginUnstructuredResourceLoader();
}
@Override
@ -35,14 +29,10 @@ public class PluginLoadedListener implements ApplicationListener<HaloPluginLoade
// load plugin.yaml
YamlPluginFinder yamlPluginFinder = new YamlPluginFinder();
Plugin plugin = yamlPluginFinder.find(pluginWrapper.getPluginPath());
DefaultResourceLoader defaultResourceLoader =
new DefaultResourceLoader(pluginWrapper.getPluginClassLoader());
extensionClient.create(plugin);
// load reverse proxy
Resource resource = defaultResourceLoader.getResource(REVERSE_PROXY_NAME);
if (resource.exists()) {
YamlUnstructuredLoader unstructuredLoader = new YamlUnstructuredLoader(resource);
unstructuredLoader.load().forEach(extensionClient::create);
}
// load plugin unstructured resource
pluginUnstructuredResourceLoader.loadUnstructured(pluginWrapper)
.forEach(extensionClient::create);
}
}

View File

@ -0,0 +1,19 @@
package run.halo.app.plugin;
import org.pf4j.PluginRuntimeException;
/**
* Exception for plugin not found.
*
* @author guqing
* @since 2.0.0
*/
public class PluginNotFoundException extends PluginRuntimeException {
public PluginNotFoundException(String message) {
super(message);
}
public PluginNotFoundException(Throwable cause) {
super(cause);
}
}

View File

@ -1,6 +1,7 @@
package run.halo.app.plugin;
import java.util.List;
import run.halo.app.core.extension.Plugin;
/**
* Service for plugin.
@ -16,4 +17,22 @@ public interface PluginService {
* @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,8 +1,13 @@
package run.halo.app.plugin;
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}.
@ -15,8 +20,15 @@ public class PluginServiceImpl implements PluginService {
private final ExtensionClient extensionClient;
public PluginServiceImpl(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;
}
/**
@ -27,4 +39,59 @@ public class PluginServiceImpl implements PluginService {
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);
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

@ -12,6 +12,7 @@ import org.pf4j.PluginDescriptor;
import org.pf4j.PluginDescriptorFinder;
import org.pf4j.util.FileUtils;
import org.springframework.util.CollectionUtils;
import run.halo.app.core.extension.Plugin;
/**
* Find a plugin descriptor for a plugin path.

View File

@ -7,9 +7,11 @@ import java.nio.file.Paths;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.pf4j.PluginRuntimeException;
import org.pf4j.PluginState;
import org.pf4j.util.FileUtils;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import run.halo.app.core.extension.Plugin;
import run.halo.app.extension.Unstructured;
import run.halo.app.infra.utils.YamlUnstructuredLoader;
@ -58,7 +60,13 @@ public class YamlPluginFinder {
}
public Plugin find(Path pluginPath) {
return readPluginDescriptor(pluginPath);
Plugin plugin = readPluginDescriptor(pluginPath);
if (plugin.getStatus() == null) {
Plugin.PluginStatus pluginStatus = new Plugin.PluginStatus();
pluginStatus.setPhase(PluginState.RESOLVED);
plugin.setStatus(pluginStatus);
}
return plugin;
}
protected Plugin readPluginDescriptor(Path pluginPath) {

View File

@ -0,0 +1,38 @@
package run.halo.app.plugin.resources;
import org.springframework.stereotype.Component;
import run.halo.app.core.extension.ReverseProxy;
/**
* TODO Optimize code to support user customize js bundle rules.
*
* @author guqing
* @since 2.0.0
*/
@Component
public class JsBundleRuleProvider {
/**
* Gets plugin js bundle rule.
*
* @param pluginName plugin name
* @return a js bundle rule
*/
public ReverseProxy.ReverseProxyRule jsRule(String pluginName) {
ReverseProxy.FileReverseProxyProvider
file = new ReverseProxy.FileReverseProxyProvider("admin", "main.js");
return new ReverseProxy.ReverseProxyRule("/admin/main.js", file);
}
/**
* Gets plugin stylesheet rule.
*
* @param pluginName plugin name
* @return a stylesheet bundle rule
*/
public ReverseProxy.ReverseProxyRule cssRule(String pluginName) {
ReverseProxy.FileReverseProxyProvider
file = new ReverseProxy.FileReverseProxyProvider("admin", "style.css");
return new ReverseProxy.ReverseProxyRule("/admin/style.css", file);
}
}

View File

@ -4,6 +4,7 @@ import static org.springframework.http.MediaType.ALL;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
@ -18,11 +19,12 @@ import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import run.halo.app.core.extension.ReverseProxy;
import run.halo.app.core.extension.ReverseProxy.FileReverseProxyProvider;
import run.halo.app.core.extension.ReverseProxy.ReverseProxyRule;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.infra.utils.PathUtils;
import run.halo.app.plugin.PluginApplicationContext;
import run.halo.app.plugin.resources.ReverseProxy.FileReverseProxyProvider;
import run.halo.app.plugin.resources.ReverseProxy.ReverseProxyRule;
/**
* <p>Plugin's reverse proxy router factory.</p>
@ -41,8 +43,12 @@ public class ReverseProxyRouterFunctionFactory {
private final ExtensionClient extensionClient;
public ReverseProxyRouterFunctionFactory(ExtensionClient extensionClient) {
private final JsBundleRuleProvider jsBundleRuleProvider;
public ReverseProxyRouterFunctionFactory(ExtensionClient extensionClient,
JsBundleRuleProvider jsBundleRuleProvider) {
this.extensionClient = extensionClient;
this.jsBundleRuleProvider = jsBundleRuleProvider;
}
/**
@ -89,7 +95,7 @@ public class ReverseProxyRouterFunctionFactory {
}
private List<ReverseProxyRule> getReverseProxyRules(String pluginId) {
return extensionClient.list(ReverseProxy.class,
List<ReverseProxyRule> rules = extensionClient.list(ReverseProxy.class,
reverseProxy -> {
String pluginName = reverseProxy.getMetadata()
.getLabels()
@ -101,9 +107,27 @@ public class ReverseProxyRouterFunctionFactory {
.map(ReverseProxy::getRules)
.flatMap(List::stream)
.collect(Collectors.toList());
// populate plugin js bundle rules.
rules.addAll(getJsBundleRules(pluginId));
return rules;
}
private String buildRoutePath(String pluginId, ReverseProxyRule reverseProxyRule) {
private List<ReverseProxyRule> getJsBundleRules(String pluginId) {
List<ReverseProxyRule> rules = new ArrayList<>(2);
ReverseProxyRule jsRule = jsBundleRuleProvider.jsRule(pluginId);
if (jsRule != null) {
rules.add(jsRule);
}
ReverseProxyRule cssRule = jsBundleRuleProvider.cssRule(pluginId);
if (cssRule != null) {
rules.add(cssRule);
}
return rules;
}
public static String buildRoutePath(String pluginId, ReverseProxyRule reverseProxyRule) {
return PathUtils.combinePath(REVERSE_PROXY_API_PREFIX, pluginId, reverseProxyRule.path());
}

View File

@ -68,19 +68,19 @@ class PluginLifeCycleManagerControllerTest {
@Test
void start() {
webClient.get()
webClient.put()
.uri(prefix + "/apples/startup")
.exchange()
.expectStatus()
.isOk();
.is5xxServerError();
}
@Test
void stop() {
webClient.get()
webClient.put()
.uri(prefix + "/apples/stop")
.exchange()
.expectStatus()
.isOk();
.is5xxServerError();
}
}

View File

@ -16,6 +16,7 @@ import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.core.io.Resource;
import org.springframework.security.util.InMemoryResource;
import org.springframework.util.ResourceUtils;
import run.halo.app.core.extension.Plugin;
import run.halo.app.extension.Unstructured;
import run.halo.app.infra.utils.JsonUtils;
@ -52,12 +53,24 @@ class YamlPluginFinderTest {
},
"homepage": "https://github.com/guqing/halo-plugin-1",
"description": "Tell me more about this plugin.",
"license": [{
"license": [
{
"name": "MIT",
"url": ""
}],
}
],
"requires": ">=2.0.0",
"pluginClass": "run.halo.app.plugin.BasePlugin"
"pluginClass": "run.halo.app.plugin.BasePlugin",
"enabled": false
},
"status": {
"phase": "RESOLVED",
"reason": null,
"message": null,
"lastStartTime": null,
"lastTransitionTime": null,
"entry": null,
"stylesheet": null
},
"apiVersion": "plugin.halo.run/v1alpha1",
"kind": "Plugin",

View File

@ -14,6 +14,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import run.halo.app.core.extension.ReverseProxy;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.Metadata;
import run.halo.app.plugin.PluginApplicationContext;
@ -37,7 +38,9 @@ class ReverseProxyRouterFunctionFactoryTest {
@BeforeEach
void setUp() {
reverseProxyRouterFunctionFactory = new ReverseProxyRouterFunctionFactory(extensionClient);
JsBundleRuleProvider jsBundleRuleProvider = new JsBundleRuleProvider();
reverseProxyRouterFunctionFactory = new ReverseProxyRouterFunctionFactory(extensionClient,
jsBundleRuleProvider);
ReverseProxy reverseProxy = mockReverseProxy();