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.stereotype.Component;
|
||||
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.authorization.Role;
|
||||
|
||||
|
@ -14,5 +15,6 @@ public class SchemeInitializer implements ApplicationListener<ApplicationStarted
|
|||
public void onApplicationEvent(ApplicationStartedEvent event) {
|
||||
Schemes.INSTANCE.register(Role.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.PluginDescriptor;
|
||||
import org.pf4j.PluginDescriptorFinder;
|
||||
import org.pf4j.PluginRepository;
|
||||
import org.pf4j.PluginRuntimeException;
|
||||
import org.pf4j.PluginState;
|
||||
import org.pf4j.PluginStateEvent;
|
||||
|
@ -59,11 +60,6 @@ public class HaloPluginManager extends DefaultPluginManager
|
|||
return new SpringExtensionFactory(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PluginDescriptorFinder getPluginDescriptorFinder() {
|
||||
return super.getPluginDescriptorFinder();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ExtensionFinder createExtensionFinder() {
|
||||
return new SpringComponentsFinder(this);
|
||||
|
@ -97,6 +93,15 @@ public class HaloPluginManager extends DefaultPluginManager
|
|||
return startingErrors.get(pluginId);
|
||||
}
|
||||
|
||||
public PluginRepository getPluginRepository() {
|
||||
return this.pluginRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PluginDescriptorFinder createPluginDescriptorFinder() {
|
||||
return new YamlPluginDescriptorFinder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> getExtensions(Class<T> type) {
|
||||
return this.getExtensions(extensionFinder.find(type));
|
||||
|
|
|
@ -7,6 +7,7 @@ import lombok.Data;
|
|||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.GVK;
|
||||
|
||||
/**
|
||||
* A custom resource for Plugin.
|
||||
|
@ -16,6 +17,8 @@ import run.halo.app.extension.AbstractExtension;
|
|||
*/
|
||||
@Data
|
||||
@ToString(callSuper = true)
|
||||
@GVK(group = "plugin.halo.run", version = "v1alpha1", kind = "Plugin", plural = "plugins",
|
||||
singular = "plugin")
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class Plugin extends AbstractExtension {
|
||||
|
||||
|
|
|
@ -23,6 +23,8 @@ import org.springframework.util.Assert;
|
|||
*/
|
||||
public class PluginApplicationContext extends GenericApplicationContext {
|
||||
|
||||
private String pluginId;
|
||||
|
||||
/**
|
||||
* <p>覆盖父类方法中判断context parent不为空时使用parent context广播事件的逻辑。
|
||||
* 如果主应用桥接事件到插件中且设置了parent会导致发布事件时死循环.</p>
|
||||
|
@ -72,4 +74,12 @@ public class PluginApplicationContext extends GenericApplicationContext {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String getPluginId() {
|
||||
return pluginId;
|
||||
}
|
||||
|
||||
public void setPluginId(String pluginId) {
|
||||
this.pluginId = pluginId;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.util.List;
|
||||
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.ApplicationListener;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
|
@ -20,27 +19,19 @@ import org.springframework.stereotype.Component;
|
|||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@ConditionalOnClass(HaloPluginManager.class)
|
||||
public class PluginApplicationEventBridgeDispatcher
|
||||
implements ApplicationListener<ApplicationEvent> {
|
||||
|
||||
private final HaloPluginManager haloPluginManager;
|
||||
|
||||
public PluginApplicationEventBridgeDispatcher(HaloPluginManager haloPluginManager) {
|
||||
this.haloPluginManager = haloPluginManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ApplicationEvent event) {
|
||||
if (!isSharedEventAnnotationPresent(event.getClass())) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (PluginWrapper startedPlugin : haloPluginManager.getStartedPlugins()) {
|
||||
PluginApplicationContext pluginApplicationContext =
|
||||
haloPluginManager.getPluginApplicationContext(startedPlugin.getPluginId());
|
||||
List<PluginApplicationContext> pluginApplicationContexts =
|
||||
ExtensionContextRegistry.getInstance().getPluginApplicationContexts();
|
||||
for (PluginApplicationContext pluginApplicationContext : pluginApplicationContexts) {
|
||||
log.debug("Bridging broadcast event [{}] to plugin [{}]", event,
|
||||
startedPlugin.getPluginId());
|
||||
pluginApplicationContext.getPluginId());
|
||||
pluginApplicationContext.publishEvent(event);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,6 +42,8 @@ public class PluginApplicationInitializer {
|
|||
PluginApplicationContext pluginApplicationContext = new PluginApplicationContext();
|
||||
pluginApplicationContext.setParent(getRootApplicationContext());
|
||||
pluginApplicationContext.setClassLoader(pluginClassLoader);
|
||||
// populate plugin to plugin application context
|
||||
pluginApplicationContext.setPluginId(pluginId);
|
||||
stopWatch.stop();
|
||||
|
||||
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