diff --git a/application/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java b/application/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java index 9cfb07909..9a54d07c0 100644 --- a/application/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java +++ b/application/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java @@ -123,6 +123,19 @@ public class PluginEndpoint implements CustomEndpoint { .response(responseBuilder() .implementation(ConfigMap.class)) ) + .PUT("plugins/{name}/reload", this::reload, + builder -> builder.operationId("reloadPlugin") + .description("Reload a plugin by name.") + .tag(tag) + .parameter(parameterBuilder() + .name("name") + .in(ParameterIn.PATH) + .required(true) + .implementation(String.class) + ) + .response(responseBuilder() + .implementation(Plugin.class)) + ) .GET("plugins", this::list, builder -> { builder.operationId("ListPlugins") .tag(tag) @@ -164,6 +177,11 @@ public class PluginEndpoint implements CustomEndpoint { .build(); } + private Mono reload(ServerRequest serverRequest) { + var name = serverRequest.pathVariable("name"); + return ServerResponse.ok().body(pluginService.reload(name), Plugin.class); + } + private Mono listPresets(ServerRequest request) { return ServerResponse.ok().body(pluginService.getPresets(), Plugin.class); } diff --git a/application/src/main/java/run/halo/app/core/extension/service/PluginService.java b/application/src/main/java/run/halo/app/core/extension/service/PluginService.java index 9aa5a23dd..2b2175290 100644 --- a/application/src/main/java/run/halo/app/core/extension/service/PluginService.java +++ b/application/src/main/java/run/halo/app/core/extension/service/PluginService.java @@ -1,6 +1,7 @@ package run.halo.app.core.extension.service; import java.nio.file.Path; +import org.springframework.web.server.ServerWebInputException; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import run.halo.app.core.extension.Plugin; @@ -27,4 +28,16 @@ public interface PluginService { Mono upgrade(String name, Path path); + /** + *

Reload a plugin by name.

+ * Note that this method will set spec.enabled to true it means that the plugin + * will be started. + * + * @param name plugin name + * @return an updated plugin reloaded from plugin path + * @throws ServerWebInputException if plugin not found by the given name + * @see Plugin.PluginSpec#setEnabled(Boolean) + * @see run.halo.app.plugin.HaloPluginManager#reloadPlugin(String) + */ + Mono reload(String name); } diff --git a/application/src/main/java/run/halo/app/core/extension/service/impl/PluginServiceImpl.java b/application/src/main/java/run/halo/app/core/extension/service/impl/PluginServiceImpl.java index c821c173c..8f4bac906 100644 --- a/application/src/main/java/run/halo/app/core/extension/service/impl/PluginServiceImpl.java +++ b/application/src/main/java/run/halo/app/core/extension/service/impl/PluginServiceImpl.java @@ -9,7 +9,9 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; import java.util.Objects; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.pf4j.PluginWrapper; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.retry.RetryException; @@ -30,11 +32,13 @@ import run.halo.app.infra.exception.PluginAlreadyExistsException; import run.halo.app.infra.exception.UnsatisfiedAttributeValueException; import run.halo.app.infra.utils.FileUtils; import run.halo.app.infra.utils.VersionUtils; +import run.halo.app.plugin.HaloPluginManager; import run.halo.app.plugin.PluginProperties; import run.halo.app.plugin.YamlPluginFinder; @Slf4j @Component +@RequiredArgsConstructor public class PluginServiceImpl implements PluginService { private static final String PRESET_LOCATION_PREFIX = "classpath:/presets/plugins/"; @@ -46,12 +50,7 @@ public class PluginServiceImpl implements PluginService { private final PluginProperties pluginProperties; - public PluginServiceImpl(ReactiveExtensionClient client, - SystemVersionSupplier systemVersion, PluginProperties pluginProperties) { - this.client = client; - this.systemVersion = systemVersion; - this.pluginProperties = pluginProperties; - } + private final HaloPluginManager pluginManager; @Override public Flux getPresets() { @@ -123,6 +122,25 @@ public class PluginServiceImpl implements PluginService { }); } + @Override + public Mono reload(String name) { + PluginWrapper pluginWrapper = pluginManager.getPlugin(name); + if (pluginWrapper == null) { + return Mono.error(() -> new ServerWebInputException( + "The given plugin with name " + name + " was not found.")); + } + YamlPluginFinder yamlPluginFinder = new YamlPluginFinder(); + Plugin newPlugin = yamlPluginFinder.find(pluginWrapper.getPluginPath()); + // reload plugin + pluginManager.reloadPlugin(name); + return client.get(Plugin.class, name) + .flatMap(plugin -> { + newPlugin.getMetadata().setVersion(plugin.getMetadata().getVersion()); + newPlugin.getSpec().setEnabled(true); + return client.update(newPlugin); + }); + } + /** * Copy plugin into plugin home. * diff --git a/application/src/main/java/run/halo/app/plugin/HaloPluginManager.java b/application/src/main/java/run/halo/app/plugin/HaloPluginManager.java index d5c5135a5..1c384437b 100644 --- a/application/src/main/java/run/halo/app/plugin/HaloPluginManager.java +++ b/application/src/main/java/run/halo/app/plugin/HaloPluginManager.java @@ -353,8 +353,14 @@ public class HaloPluginManager extends DefaultPluginManager } /** - * Reload plugin by id,it will be clean up memory resources of plugin and reload plugin from - * disk. + *

Reload plugin by id,it will be clean up memory resources of plugin and reload plugin from + * disk.

+ *

+ * Note: This method will not start plugin, you need to start plugin manually. + * this is to avoid starting plugins in different places, which will cause thread safety + * issues, so all of them are handed over to the + * {@link run.halo.app.core.extension.reconciler.PluginReconciler} to start the plugin + *

* * @param pluginId plugin id * @return plugin startup status @@ -368,8 +374,7 @@ public class HaloPluginManager extends DefaultPluginManager } catch (Exception ex) { return null; } - - return doStartPlugin(pluginId); + return getPlugin(pluginId).getPluginState(); } /** diff --git a/application/src/main/resources/extensions/role-template-plugin.yaml b/application/src/main/resources/extensions/role-template-plugin.yaml index 1cb06bc2e..70a881545 100644 --- a/application/src/main/resources/extensions/role-template-plugin.yaml +++ b/application/src/main/resources/extensions/role-template-plugin.yaml @@ -16,7 +16,7 @@ rules: resources: [ "plugins" ] verbs: [ "create", "patch", "update", "delete", "deletecollection" ] - apiGroups: [ "api.console.halo.run" ] - resources: [ "plugins/upgrade", "plugins/resetconfig", "plugins/config" ] + resources: [ "plugins/upgrade", "plugins/resetconfig", "plugins/config", "plugins/reload" ] verbs: [ "*" ] - apiGroups: [ "api.console.halo.run" ] resources: [ "plugin-presets" ] diff --git a/application/src/test/java/run/halo/app/core/extension/service/impl/PluginServiceImplTest.java b/application/src/test/java/run/halo/app/core/extension/service/impl/PluginServiceImplTest.java index d0e4f786b..df4ea809b 100644 --- a/application/src/test/java/run/halo/app/core/extension/service/impl/PluginServiceImplTest.java +++ b/application/src/test/java/run/halo/app/core/extension/service/impl/PluginServiceImplTest.java @@ -1,9 +1,12 @@ package run.halo.app.core.extension.service.impl; import static java.util.Objects.requireNonNull; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; @@ -23,19 +26,23 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.pf4j.PluginState; +import org.pf4j.PluginWrapper; import org.springframework.web.server.ServerErrorException; import org.springframework.web.server.ServerWebInputException; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import run.halo.app.core.extension.Plugin; +import run.halo.app.extension.Metadata; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.infra.SystemVersionSupplier; import run.halo.app.infra.exception.PluginAlreadyExistsException; import run.halo.app.infra.utils.FileUtils; +import run.halo.app.plugin.HaloPluginManager; import run.halo.app.plugin.PluginProperties; import run.halo.app.plugin.YamlPluginFinder; @@ -51,6 +58,9 @@ class PluginServiceImplTest { @Mock PluginProperties pluginProperties; + @Mock + HaloPluginManager pluginManager; + @InjectMocks PluginServiceImpl pluginService; @@ -204,4 +214,45 @@ class PluginServiceImplTest { verify(client).create(argThat(p -> p.getSpec().getEnabled())); } } + + @Test + void reload() throws IOException, URISyntaxException { + var fakePluginUri = requireNonNull( + getClass().getClassLoader().getResource("plugin/plugin-0.0.2")).toURI(); + Path tempDirectory = Files.createTempDirectory("halo-ut-plugin-service-impl-"); + Path fakePluginPath = tempDirectory.resolve("plugin-0.0.2.jar"); + try { + FileUtils.jar(Paths.get(fakePluginUri), tempDirectory.resolve("plugin-0.0.2.jar")); + + final String pluginName = "fake-plugin"; + PluginWrapper pluginWrapper = mock(PluginWrapper.class); + when(pluginManager.getPlugin(eq(pluginName))).thenReturn(pluginWrapper); + when(pluginWrapper.getPluginPath()).thenReturn(fakePluginPath); + + Plugin plugin = new Plugin(); + plugin.setMetadata(new Metadata()); + plugin.getMetadata().setName(pluginName); + plugin.setSpec(new Plugin.PluginSpec()); + plugin.getSpec().setEnabled(false); + plugin.getSpec().setDisplayName("Fake Plugin"); + + when(client.get(eq(Plugin.class), eq(pluginName))).thenReturn(Mono.just(plugin)); + when(client.update(any(Plugin.class))).thenReturn(Mono.empty()); + + pluginService.reload(pluginName).block(); + + verify(pluginManager).reloadPlugin(eq(pluginName)); + verify(client).get(eq(Plugin.class), eq(pluginName)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Plugin.class); + verify(client).update(captor.capture()); + + Plugin updatedPlugin = captor.getValue(); + assertThat(updatedPlugin.getSpec().getEnabled()).isTrue(); + assertThat(updatedPlugin.getSpec().getDisplayName()).isEqualTo("Fake Display Name"); + assertThat(updatedPlugin.getSpec().getDescription()).isEqualTo("Fake description"); + } finally { + FileUtils.deleteRecursivelyAndSilently(tempDirectory); + } + } } \ No newline at end of file