mirror of https://github.com/halo-dev/halo
feat: add plugin life cycle management (#2134)
* feat: add plugin life cycle management * refactor: plugin lifecycle endpoint mappingpull/2154/head
parent
95c6caaca1
commit
a2f49c60bc
|
@ -4,6 +4,7 @@ import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||||
import org.springframework.context.ApplicationListener;
|
import org.springframework.context.ApplicationListener;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import run.halo.app.extension.Schemes;
|
import run.halo.app.extension.Schemes;
|
||||||
|
import run.halo.app.plugin.Plugin;
|
||||||
import run.halo.app.security.authentication.pat.PersonalAccessToken;
|
import run.halo.app.security.authentication.pat.PersonalAccessToken;
|
||||||
import run.halo.app.security.authorization.Role;
|
import run.halo.app.security.authorization.Role;
|
||||||
|
|
||||||
|
@ -14,5 +15,6 @@ public class SchemeInitializer implements ApplicationListener<ApplicationStarted
|
||||||
public void onApplicationEvent(ApplicationStartedEvent event) {
|
public void onApplicationEvent(ApplicationStartedEvent event) {
|
||||||
Schemes.INSTANCE.register(Role.class);
|
Schemes.INSTANCE.register(Role.class);
|
||||||
Schemes.INSTANCE.register(PersonalAccessToken.class);
|
Schemes.INSTANCE.register(PersonalAccessToken.class);
|
||||||
|
Schemes.INSTANCE.register(Plugin.class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import org.pf4j.ExtensionFinder;
|
||||||
import org.pf4j.PluginDependency;
|
import org.pf4j.PluginDependency;
|
||||||
import org.pf4j.PluginDescriptor;
|
import org.pf4j.PluginDescriptor;
|
||||||
import org.pf4j.PluginDescriptorFinder;
|
import org.pf4j.PluginDescriptorFinder;
|
||||||
|
import org.pf4j.PluginRepository;
|
||||||
import org.pf4j.PluginRuntimeException;
|
import org.pf4j.PluginRuntimeException;
|
||||||
import org.pf4j.PluginState;
|
import org.pf4j.PluginState;
|
||||||
import org.pf4j.PluginStateEvent;
|
import org.pf4j.PluginStateEvent;
|
||||||
|
@ -59,11 +60,6 @@ public class HaloPluginManager extends DefaultPluginManager
|
||||||
return new SpringExtensionFactory(this);
|
return new SpringExtensionFactory(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public PluginDescriptorFinder getPluginDescriptorFinder() {
|
|
||||||
return super.getPluginDescriptorFinder();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected ExtensionFinder createExtensionFinder() {
|
protected ExtensionFinder createExtensionFinder() {
|
||||||
return new SpringComponentsFinder(this);
|
return new SpringComponentsFinder(this);
|
||||||
|
@ -97,6 +93,15 @@ public class HaloPluginManager extends DefaultPluginManager
|
||||||
return startingErrors.get(pluginId);
|
return startingErrors.get(pluginId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PluginRepository getPluginRepository() {
|
||||||
|
return this.pluginRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PluginDescriptorFinder createPluginDescriptorFinder() {
|
||||||
|
return new YamlPluginDescriptorFinder();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> List<T> getExtensions(Class<T> type) {
|
public <T> List<T> getExtensions(Class<T> type) {
|
||||||
return this.getExtensions(extensionFinder.find(type));
|
return this.getExtensions(extensionFinder.find(type));
|
||||||
|
|
|
@ -7,6 +7,7 @@ import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
import run.halo.app.extension.AbstractExtension;
|
import run.halo.app.extension.AbstractExtension;
|
||||||
|
import run.halo.app.extension.GVK;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A custom resource for Plugin.
|
* A custom resource for Plugin.
|
||||||
|
@ -16,6 +17,8 @@ import run.halo.app.extension.AbstractExtension;
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@ToString(callSuper = true)
|
@ToString(callSuper = true)
|
||||||
|
@GVK(group = "plugin.halo.run", version = "v1alpha1", kind = "Plugin", plural = "plugins",
|
||||||
|
singular = "plugin")
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public class Plugin extends AbstractExtension {
|
public class Plugin extends AbstractExtension {
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,8 @@ import org.springframework.util.Assert;
|
||||||
*/
|
*/
|
||||||
public class PluginApplicationContext extends GenericApplicationContext {
|
public class PluginApplicationContext extends GenericApplicationContext {
|
||||||
|
|
||||||
|
private String pluginId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>覆盖父类方法中判断context parent不为空时使用parent context广播事件的逻辑。
|
* <p>覆盖父类方法中判断context parent不为空时使用parent context广播事件的逻辑。
|
||||||
* 如果主应用桥接事件到插件中且设置了parent会导致发布事件时死循环.</p>
|
* 如果主应用桥接事件到插件中且设置了parent会导致发布事件时死循环.</p>
|
||||||
|
@ -72,4 +74,12 @@ public class PluginApplicationContext extends GenericApplicationContext {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getPluginId() {
|
||||||
|
return pluginId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPluginId(String pluginId) {
|
||||||
|
this.pluginId = pluginId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
package run.halo.app.plugin;
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
import java.lang.reflect.AnnotatedElement;
|
import java.lang.reflect.AnnotatedElement;
|
||||||
|
import java.util.List;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.pf4j.PluginWrapper;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
|
||||||
import org.springframework.context.ApplicationEvent;
|
import org.springframework.context.ApplicationEvent;
|
||||||
import org.springframework.context.ApplicationListener;
|
import org.springframework.context.ApplicationListener;
|
||||||
import org.springframework.core.annotation.AnnotationUtils;
|
import org.springframework.core.annotation.AnnotationUtils;
|
||||||
|
@ -20,27 +19,19 @@ import org.springframework.stereotype.Component;
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@ConditionalOnClass(HaloPluginManager.class)
|
|
||||||
public class PluginApplicationEventBridgeDispatcher
|
public class PluginApplicationEventBridgeDispatcher
|
||||||
implements ApplicationListener<ApplicationEvent> {
|
implements ApplicationListener<ApplicationEvent> {
|
||||||
|
|
||||||
private final HaloPluginManager haloPluginManager;
|
|
||||||
|
|
||||||
public PluginApplicationEventBridgeDispatcher(HaloPluginManager haloPluginManager) {
|
|
||||||
this.haloPluginManager = haloPluginManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onApplicationEvent(ApplicationEvent event) {
|
public void onApplicationEvent(ApplicationEvent event) {
|
||||||
if (!isSharedEventAnnotationPresent(event.getClass())) {
|
if (!isSharedEventAnnotationPresent(event.getClass())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
List<PluginApplicationContext> pluginApplicationContexts =
|
||||||
for (PluginWrapper startedPlugin : haloPluginManager.getStartedPlugins()) {
|
ExtensionContextRegistry.getInstance().getPluginApplicationContexts();
|
||||||
PluginApplicationContext pluginApplicationContext =
|
for (PluginApplicationContext pluginApplicationContext : pluginApplicationContexts) {
|
||||||
haloPluginManager.getPluginApplicationContext(startedPlugin.getPluginId());
|
|
||||||
log.debug("Bridging broadcast event [{}] to plugin [{}]", event,
|
log.debug("Bridging broadcast event [{}] to plugin [{}]", event,
|
||||||
startedPlugin.getPluginId());
|
pluginApplicationContext.getPluginId());
|
||||||
pluginApplicationContext.publishEvent(event);
|
pluginApplicationContext.publishEvent(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,8 @@ public class PluginApplicationInitializer {
|
||||||
PluginApplicationContext pluginApplicationContext = new PluginApplicationContext();
|
PluginApplicationContext pluginApplicationContext = new PluginApplicationContext();
|
||||||
pluginApplicationContext.setParent(getRootApplicationContext());
|
pluginApplicationContext.setParent(getRootApplicationContext());
|
||||||
pluginApplicationContext.setClassLoader(pluginClassLoader);
|
pluginApplicationContext.setClassLoader(pluginClassLoader);
|
||||||
|
// populate plugin to plugin application context
|
||||||
|
pluginApplicationContext.setPluginId(pluginId);
|
||||||
stopWatch.stop();
|
stopWatch.stop();
|
||||||
|
|
||||||
stopWatch.start("Create DefaultResourceLoader");
|
stopWatch.start("Create DefaultResourceLoader");
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
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.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 PluginServiceImpl pluginService;
|
||||||
|
private final HaloPluginManager pluginManager;
|
||||||
|
|
||||||
|
public PluginLifeCycleManagerController(PluginServiceImpl pluginService,
|
||||||
|
HaloPluginManager pluginManager) {
|
||||||
|
this.pluginService = pluginService;
|
||||||
|
this.pluginManager = pluginManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public List<Plugin> list() {
|
||||||
|
return pluginService.list();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{pluginName}/startup")
|
||||||
|
public PluginState start(@PathVariable String pluginName) {
|
||||||
|
return pluginManager.startPlugin(pluginName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{pluginName}/stop")
|
||||||
|
public PluginState stop(@PathVariable String pluginName) {
|
||||||
|
return pluginManager.stopPlugin(pluginName);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for plugin.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
public interface PluginService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists plugin from repository.
|
||||||
|
*
|
||||||
|
* @return all loaded plugins.
|
||||||
|
*/
|
||||||
|
List<Plugin> list();
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import run.halo.app.extension.ExtensionClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of {@link PluginService}.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class PluginServiceImpl implements PluginService {
|
||||||
|
|
||||||
|
private final ExtensionClient extensionClient;
|
||||||
|
|
||||||
|
public PluginServiceImpl(ExtensionClient extensionClient) {
|
||||||
|
this.extensionClient = extensionClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* list all plugins including loaded and unloaded.
|
||||||
|
*
|
||||||
|
* @return plugin info
|
||||||
|
*/
|
||||||
|
public List<Plugin> list() {
|
||||||
|
return extensionClient.list(Plugin.class, null, null);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
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.extension.Metadata;
|
||||||
|
import run.halo.app.security.authorization.DefaultRoleGetter;
|
||||||
|
import run.halo.app.security.authorization.PolicyRule;
|
||||||
|
import run.halo.app.security.authorization.Role;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@SpringBootTest
|
||||||
|
@WithMockUser(username = "user")
|
||||||
|
@AutoConfigureWebTestClient
|
||||||
|
class PluginLifeCycleManagerControllerTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
WebTestClient webClient;
|
||||||
|
@MockBean
|
||||||
|
DefaultRoleGetter defaultRoleGetter;
|
||||||
|
|
||||||
|
@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);
|
||||||
|
PolicyRule policyRule = new PolicyRule.Builder()
|
||||||
|
.apiGroups("plugin.halo.run")
|
||||||
|
.resources("plugins", "plugins/startup", "plugins/stop")
|
||||||
|
.verbs("*")
|
||||||
|
.build();
|
||||||
|
role.setRules(List.of(policyRule));
|
||||||
|
when(defaultRoleGetter.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.get()
|
||||||
|
.uri(prefix + "/apples/startup")
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.isOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void stop() {
|
||||||
|
webClient.get()
|
||||||
|
.uri(prefix + "/apples/stop")
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.isOk();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue