feat: add plugin life cycle management (#2134)

* feat: add plugin life cycle management

* refactor: plugin lifecycle endpoint mapping
pull/2154/head
guqing 2022-06-13 10:36:11 +08:00 committed by GitHub
parent 95c6caaca1
commit a2f49c60bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 213 additions and 19 deletions

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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