mirror of https://github.com/halo-dev/halo
refactor: the way of router function register for reverse proxy (#2522)
#### What type of PR is this? /kind improvement /area core /milestone 2.0 #### What this PR does / why we need it: 重构插件反向代理注册方式 #### Which issue(s) this PR fixes: Fixes #2520 #### Special notes for your reviewer: How to test: 1. clone 项目 https://github.com/halo-sigs/plugin-comment-widget 后 build 一个 jar 2. 将该 jar 作为一个插件安装到系统并启用它 3. 访问插件提供的反向代理资源 ``` curl --location --request GET 'http://localhost:8090/assets/PluginCommentWidget/static/comment-widget.iife.js' ``` 期望得到结果 4. 停用插件,重复 Step 3,期望得到 404 5. 重复数次 Step 3-4 均与期望相同即可 #### Does this PR introduce a user-facing change? ```release-note None ```pull/2524/head
parent
a4609f68d1
commit
04300308fe
|
@ -17,6 +17,7 @@ import run.halo.app.core.extension.Menu;
|
||||||
import run.halo.app.core.extension.MenuItem;
|
import run.halo.app.core.extension.MenuItem;
|
||||||
import run.halo.app.core.extension.Plugin;
|
import run.halo.app.core.extension.Plugin;
|
||||||
import run.halo.app.core.extension.Post;
|
import run.halo.app.core.extension.Post;
|
||||||
|
import run.halo.app.core.extension.ReverseProxy;
|
||||||
import run.halo.app.core.extension.Role;
|
import run.halo.app.core.extension.Role;
|
||||||
import run.halo.app.core.extension.RoleBinding;
|
import run.halo.app.core.extension.RoleBinding;
|
||||||
import run.halo.app.core.extension.SinglePage;
|
import run.halo.app.core.extension.SinglePage;
|
||||||
|
@ -30,6 +31,7 @@ import run.halo.app.core.extension.reconciler.MenuItemReconciler;
|
||||||
import run.halo.app.core.extension.reconciler.MenuReconciler;
|
import run.halo.app.core.extension.reconciler.MenuReconciler;
|
||||||
import run.halo.app.core.extension.reconciler.PluginReconciler;
|
import run.halo.app.core.extension.reconciler.PluginReconciler;
|
||||||
import run.halo.app.core.extension.reconciler.PostReconciler;
|
import run.halo.app.core.extension.reconciler.PostReconciler;
|
||||||
|
import run.halo.app.core.extension.reconciler.ReverseProxyReconciler;
|
||||||
import run.halo.app.core.extension.reconciler.RoleBindingReconciler;
|
import run.halo.app.core.extension.reconciler.RoleBindingReconciler;
|
||||||
import run.halo.app.core.extension.reconciler.RoleReconciler;
|
import run.halo.app.core.extension.reconciler.RoleReconciler;
|
||||||
import run.halo.app.core.extension.reconciler.SinglePageReconciler;
|
import run.halo.app.core.extension.reconciler.SinglePageReconciler;
|
||||||
|
@ -56,6 +58,7 @@ import run.halo.app.infra.properties.HaloProperties;
|
||||||
import run.halo.app.plugin.ExtensionComponentsFinder;
|
import run.halo.app.plugin.ExtensionComponentsFinder;
|
||||||
import run.halo.app.plugin.HaloPluginManager;
|
import run.halo.app.plugin.HaloPluginManager;
|
||||||
import run.halo.app.plugin.resources.JsBundleRuleProvider;
|
import run.halo.app.plugin.resources.JsBundleRuleProvider;
|
||||||
|
import run.halo.app.plugin.resources.ReverseProxyRouterFunctionRegistry;
|
||||||
import run.halo.app.theme.router.TemplateRouteManager;
|
import run.halo.app.theme.router.TemplateRouteManager;
|
||||||
|
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ -212,6 +215,15 @@ public class ExtensionConfiguration {
|
||||||
.extension(new Comment())
|
.extension(new Comment())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
Controller reverseProxyController(ExtensionClient client,
|
||||||
|
ReverseProxyRouterFunctionRegistry reverseProxyRouterFunctionRegistry) {
|
||||||
|
return new ControllerBuilder("reverse-proxy-controller", client)
|
||||||
|
.reconciler(new ReverseProxyReconciler(client, reverseProxyRouterFunctionRegistry))
|
||||||
|
.extension(new ReverseProxy())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
package run.halo.app.core.extension.reconciler;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import run.halo.app.core.extension.ReverseProxy;
|
||||||
|
import run.halo.app.extension.ExtensionClient;
|
||||||
|
import run.halo.app.extension.controller.Reconciler;
|
||||||
|
import run.halo.app.plugin.PluginConst;
|
||||||
|
import run.halo.app.plugin.resources.ReverseProxyRouterFunctionRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconciler for {@link ReverseProxy}.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
public class ReverseProxyReconciler implements Reconciler<Reconciler.Request> {
|
||||||
|
private static final String FINALIZER_NAME = "reverse-proxy-protection";
|
||||||
|
private final ExtensionClient client;
|
||||||
|
private final ReverseProxyRouterFunctionRegistry routerFunctionRegistry;
|
||||||
|
|
||||||
|
public ReverseProxyReconciler(ExtensionClient client,
|
||||||
|
ReverseProxyRouterFunctionRegistry routerFunctionRegistry) {
|
||||||
|
this.client = client;
|
||||||
|
this.routerFunctionRegistry = routerFunctionRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result reconcile(Request request) {
|
||||||
|
return client.fetch(ReverseProxy.class, request.name())
|
||||||
|
.map(reverseProxy -> {
|
||||||
|
if (isDeleted(reverseProxy)) {
|
||||||
|
cleanUpResourcesAndRemoveFinalizer(request.name());
|
||||||
|
return new Result(false, null);
|
||||||
|
}
|
||||||
|
addFinalizerIfNecessary(reverseProxy);
|
||||||
|
registerReverseProxy(reverseProxy);
|
||||||
|
return new Result(false, null);
|
||||||
|
})
|
||||||
|
.orElse(new Result(false, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerReverseProxy(ReverseProxy reverseProxy) {
|
||||||
|
String pluginId = getPluginId(reverseProxy);
|
||||||
|
routerFunctionRegistry.register(pluginId, reverseProxy).block();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanUpResources(ReverseProxy reverseProxy) {
|
||||||
|
String pluginId = getPluginId(reverseProxy);
|
||||||
|
routerFunctionRegistry.remove(pluginId).block();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addFinalizerIfNecessary(ReverseProxy oldReverseProxy) {
|
||||||
|
Set<String> finalizers = oldReverseProxy.getMetadata().getFinalizers();
|
||||||
|
if (finalizers != null && finalizers.contains(FINALIZER_NAME)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
client.fetch(ReverseProxy.class, oldReverseProxy.getMetadata().getName())
|
||||||
|
.ifPresent(reverseProxy -> {
|
||||||
|
Set<String> newFinalizers = reverseProxy.getMetadata().getFinalizers();
|
||||||
|
if (newFinalizers == null) {
|
||||||
|
newFinalizers = new HashSet<>();
|
||||||
|
reverseProxy.getMetadata().setFinalizers(newFinalizers);
|
||||||
|
}
|
||||||
|
newFinalizers.add(FINALIZER_NAME);
|
||||||
|
client.update(reverseProxy);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanUpResourcesAndRemoveFinalizer(String name) {
|
||||||
|
client.fetch(ReverseProxy.class, name).ifPresent(reverseProxy -> {
|
||||||
|
cleanUpResources(reverseProxy);
|
||||||
|
if (reverseProxy.getMetadata().getFinalizers() != null) {
|
||||||
|
reverseProxy.getMetadata().getFinalizers().remove(FINALIZER_NAME);
|
||||||
|
}
|
||||||
|
client.update(reverseProxy);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDeleted(ReverseProxy reverseProxy) {
|
||||||
|
return reverseProxy.getMetadata().getDeletionTimestamp() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getPluginId(ReverseProxy reverseProxy) {
|
||||||
|
Map<String, String> labels = reverseProxy.getMetadata().getLabels();
|
||||||
|
if (labels == null) {
|
||||||
|
return PluginConst.SYSTEM_PLUGIN_NAME;
|
||||||
|
}
|
||||||
|
return StringUtils.defaultString(labels.get(PluginConst.PLUGIN_NAME_LABEL_NAME),
|
||||||
|
PluginConst.SYSTEM_PLUGIN_NAME);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +1,9 @@
|
||||||
package run.halo.app.plugin;
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.stream.Stream;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import org.pf4j.PluginWrapper;
|
|
||||||
import org.springframework.context.event.EventListener;
|
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.CollectionUtils;
|
|
||||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||||
|
@ -17,9 +11,7 @@ import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.plugin.event.HaloPluginStartedEvent;
|
import run.halo.app.plugin.resources.ReverseProxyRouterFunctionRegistry;
|
||||||
import run.halo.app.plugin.event.HaloPluginStoppedEvent;
|
|
||||||
import run.halo.app.plugin.resources.ReverseProxyRouterFunctionFactory;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A composite {@link RouterFunction} implementation for plugin.
|
* A composite {@link RouterFunction} implementation for plugin.
|
||||||
|
@ -30,69 +22,37 @@ import run.halo.app.plugin.resources.ReverseProxyRouterFunctionFactory;
|
||||||
@Component
|
@Component
|
||||||
public class PluginCompositeRouterFunction implements RouterFunction<ServerResponse> {
|
public class PluginCompositeRouterFunction implements RouterFunction<ServerResponse> {
|
||||||
|
|
||||||
private final Map<String, RouterFunction<ServerResponse>> routerFunctionRegistry =
|
private final ReverseProxyRouterFunctionRegistry reverseProxyRouterFunctionFactory;
|
||||||
new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
private final ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory;
|
|
||||||
|
|
||||||
public PluginCompositeRouterFunction(
|
public PluginCompositeRouterFunction(
|
||||||
ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory) {
|
ReverseProxyRouterFunctionRegistry reverseProxyRouterFunctionFactory) {
|
||||||
this.reverseProxyRouterFunctionFactory = reverseProxyRouterFunctionFactory;
|
this.reverseProxyRouterFunctionFactory = reverseProxyRouterFunctionFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RouterFunction<ServerResponse> getRouterFunction(String pluginId) {
|
|
||||||
return routerFunctionRegistry.get(pluginId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@NonNull
|
@NonNull
|
||||||
public Mono<HandlerFunction<ServerResponse>> route(@NonNull ServerRequest request) {
|
public Mono<HandlerFunction<ServerResponse>> route(@NonNull ServerRequest request) {
|
||||||
return Flux.fromIterable(routerFunctionRegistry.values())
|
return Flux.fromIterable(routerFunctions())
|
||||||
.concatMap(routerFunction -> routerFunction.route(request))
|
.concatMap(routerFunction -> routerFunction.route(request))
|
||||||
.next();
|
.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void accept(@NonNull RouterFunctions.Visitor visitor) {
|
public void accept(@NonNull RouterFunctions.Visitor visitor) {
|
||||||
routerFunctionRegistry.values().forEach(routerFunction -> routerFunction.accept(visitor));
|
routerFunctions().forEach(routerFunction -> routerFunction.accept(visitor));
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtains the user-defined {@link RouterFunction} from the plugin
|
|
||||||
* {@link PluginApplicationContext} and create {@link RouterFunction} according to the
|
|
||||||
* reverse proxy configuration file then register them to {@link #routerFunctionRegistry}.
|
|
||||||
*
|
|
||||||
* @param haloPluginStartedEvent event for plugin started
|
|
||||||
*/
|
|
||||||
@EventListener(HaloPluginStartedEvent.class)
|
|
||||||
public Mono<Void> onPluginStarted(HaloPluginStartedEvent haloPluginStartedEvent) {
|
|
||||||
PluginWrapper plugin = haloPluginStartedEvent.getPlugin();
|
|
||||||
// Obtain plugin application context
|
|
||||||
PluginApplicationContext pluginApplicationContext =
|
|
||||||
ExtensionContextRegistry.getInstance().getByPluginId(plugin.getPluginId());
|
|
||||||
|
|
||||||
return Flux.fromIterable(routerFunctions(pluginApplicationContext))
|
|
||||||
.concatWith(reverseProxyRouterFunctionFactory.create(pluginApplicationContext))
|
|
||||||
.reduce(RouterFunction::and)
|
|
||||||
.doOnNext(routerFunction ->
|
|
||||||
routerFunctionRegistry.put(plugin.getPluginId(), routerFunction))
|
|
||||||
.then();
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventListener(HaloPluginStoppedEvent.class)
|
|
||||||
public void onPluginStopped(HaloPluginStoppedEvent haloPluginStoppedEvent) {
|
|
||||||
PluginWrapper plugin = haloPluginStoppedEvent.getPlugin();
|
|
||||||
routerFunctionRegistry.remove(plugin.getPluginId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private List<RouterFunction<ServerResponse>> routerFunctions(
|
private List<RouterFunction<ServerResponse>> routerFunctions() {
|
||||||
PluginApplicationContext applicationContext) {
|
Stream<RouterFunction<ServerResponse>> routerFunctionStream =
|
||||||
List<RouterFunction<ServerResponse>> functions =
|
ExtensionContextRegistry.getInstance().getPluginApplicationContexts()
|
||||||
applicationContext.getBeanProvider(RouterFunction.class)
|
.stream()
|
||||||
.orderedStream()
|
.flatMap(applicationContext -> applicationContext
|
||||||
.map(router -> (RouterFunction<ServerResponse>) router)
|
.getBeanProvider(RouterFunction.class)
|
||||||
.collect(Collectors.toList());
|
.orderedStream())
|
||||||
return (!CollectionUtils.isEmpty(functions) ? functions : Collections.emptyList());
|
.map(router -> (RouterFunction<ServerResponse>) router);
|
||||||
|
return Stream.concat(routerFunctionStream,
|
||||||
|
reverseProxyRouterFunctionFactory.getRouterFunctions().stream())
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,4 +11,6 @@ public interface PluginConst {
|
||||||
* Plugin metadata labels key.
|
* Plugin metadata labels key.
|
||||||
*/
|
*/
|
||||||
String PLUGIN_NAME_LABEL_NAME = "plugin.halo.run/plugin-name";
|
String PLUGIN_NAME_LABEL_NAME = "plugin.halo.run/plugin-name";
|
||||||
|
|
||||||
|
String SYSTEM_PLUGIN_NAME = "system";
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,9 @@ import static org.springframework.web.reactive.function.server.RequestPredicates
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Predicate;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
@ -23,7 +23,6 @@ import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.ReverseProxy;
|
import run.halo.app.core.extension.ReverseProxy;
|
||||||
import run.halo.app.core.extension.ReverseProxy.FileReverseProxyProvider;
|
import run.halo.app.core.extension.ReverseProxy.FileReverseProxyProvider;
|
||||||
import run.halo.app.core.extension.ReverseProxy.ReverseProxyRule;
|
import run.halo.app.core.extension.ReverseProxy.ReverseProxyRule;
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
|
||||||
import run.halo.app.infra.utils.PathUtils;
|
import run.halo.app.infra.utils.PathUtils;
|
||||||
import run.halo.app.plugin.PluginApplicationContext;
|
import run.halo.app.plugin.PluginApplicationContext;
|
||||||
import run.halo.app.plugin.PluginConst;
|
import run.halo.app.plugin.PluginConst;
|
||||||
|
@ -41,13 +40,9 @@ import run.halo.app.plugin.PluginConst;
|
||||||
public class ReverseProxyRouterFunctionFactory {
|
public class ReverseProxyRouterFunctionFactory {
|
||||||
private static final String REVERSE_PROXY_API_PREFIX = "/assets";
|
private static final String REVERSE_PROXY_API_PREFIX = "/assets";
|
||||||
|
|
||||||
private final ReactiveExtensionClient extensionClient;
|
|
||||||
|
|
||||||
private final JsBundleRuleProvider jsBundleRuleProvider;
|
private final JsBundleRuleProvider jsBundleRuleProvider;
|
||||||
|
|
||||||
public ReverseProxyRouterFunctionFactory(ReactiveExtensionClient extensionClient,
|
public ReverseProxyRouterFunctionFactory(JsBundleRuleProvider jsBundleRuleProvider) {
|
||||||
JsBundleRuleProvider jsBundleRuleProvider) {
|
|
||||||
this.extensionClient = extensionClient;
|
|
||||||
this.jsBundleRuleProvider = jsBundleRuleProvider;
|
this.jsBundleRuleProvider = jsBundleRuleProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,21 +52,22 @@ public class ReverseProxyRouterFunctionFactory {
|
||||||
* <p>Note that: returns {@code Null} if the plugin does not have a {@link ReverseProxy} custom
|
* <p>Note that: returns {@code Null} if the plugin does not have a {@link ReverseProxy} custom
|
||||||
* resource.</p>
|
* resource.</p>
|
||||||
*
|
*
|
||||||
* @param pluginApplicationContext plugin application context
|
* @param applicationContext plugin application context or system application context
|
||||||
* @return A reverse proxy RouterFunction handle(nullable)
|
* @return A reverse proxy RouterFunction handle(nullable)
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public Mono<RouterFunction<ServerResponse>> create(
|
public Mono<RouterFunction<ServerResponse>> create(ReverseProxy reverseProxy,
|
||||||
PluginApplicationContext pluginApplicationContext) {
|
ApplicationContext applicationContext) {
|
||||||
return createReverseProxyRouterFunction(pluginApplicationContext);
|
return createReverseProxyRouterFunction(reverseProxy, applicationContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<RouterFunction<ServerResponse>> createReverseProxyRouterFunction(
|
private Mono<RouterFunction<ServerResponse>> createReverseProxyRouterFunction(
|
||||||
PluginApplicationContext pluginApplicationContext) {
|
ReverseProxy reverseProxy,
|
||||||
Assert.notNull(pluginApplicationContext, "The pluginApplicationContext must not be null.");
|
ApplicationContext applicationContext) {
|
||||||
|
Assert.notNull(reverseProxy, "The reverseProxy must not be null.");
|
||||||
var pluginId = pluginApplicationContext.getPluginId();
|
Assert.notNull(applicationContext, "The applicationContext must not be null.");
|
||||||
var rules = getReverseProxyRules(pluginId);
|
final var pluginId = getPluginId(applicationContext);
|
||||||
|
var rules = getReverseProxyRules(pluginId, reverseProxy);
|
||||||
|
|
||||||
return rules.map(rule -> {
|
return rules.map(rule -> {
|
||||||
String routePath = buildRoutePath(pluginId, rule);
|
String routePath = buildRoutePath(pluginId, rule);
|
||||||
|
@ -80,7 +76,7 @@ public class ReverseProxyRouterFunctionFactory {
|
||||||
return RouterFunctions.route(GET(routePath).and(accept(ALL)),
|
return RouterFunctions.route(GET(routePath).and(accept(ALL)),
|
||||||
request -> {
|
request -> {
|
||||||
Resource resource =
|
Resource resource =
|
||||||
loadResourceByFileRule(pluginApplicationContext, rule, request);
|
loadResourceByFileRule(pluginId, applicationContext, rule, request);
|
||||||
if (!resource.exists()) {
|
if (!resource.exists()) {
|
||||||
return ServerResponse.notFound().build();
|
return ServerResponse.notFound().build();
|
||||||
}
|
}
|
||||||
|
@ -90,22 +86,17 @@ public class ReverseProxyRouterFunctionFactory {
|
||||||
}).reduce(RouterFunction::and);
|
}).reduce(RouterFunction::and);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Flux<ReverseProxyRule> getReverseProxyRules(String pluginId) {
|
private String getPluginId(ApplicationContext applicationContext) {
|
||||||
return extensionClient.list(ReverseProxy.class, hasPluginId(pluginId), null)
|
if (applicationContext instanceof PluginApplicationContext pluginApplicationContext) {
|
||||||
.map(ReverseProxy::getRules)
|
return pluginApplicationContext.getPluginId();
|
||||||
.flatMapIterable(rules -> rules)
|
}
|
||||||
.concatWith(Flux.fromIterable(getJsBundleRules(pluginId)));
|
return PluginConst.SYSTEM_PLUGIN_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Predicate<ReverseProxy> hasPluginId(String pluginId) {
|
private Flux<ReverseProxyRule> getReverseProxyRules(String pluginId,
|
||||||
return proxy -> {
|
ReverseProxy reverseProxy) {
|
||||||
var labels = proxy.getMetadata().getLabels();
|
return Flux.fromIterable(reverseProxy.getRules())
|
||||||
if (labels == null) {
|
.concatWith(Flux.fromIterable(getJsBundleRules(pluginId)));
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var pluginName = labels.get(PluginConst.PLUGIN_NAME_LABEL_NAME);
|
|
||||||
return pluginId.equals(pluginName);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ReverseProxyRule> getJsBundleRules(String pluginId) {
|
private List<ReverseProxyRule> getJsBundleRules(String pluginId) {
|
||||||
|
@ -136,7 +127,8 @@ public class ReverseProxyRouterFunctionFactory {
|
||||||
* @return a Resource handle for the specified resource location by the plugin(never null);
|
* @return a Resource handle for the specified resource location by the plugin(never null);
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
private Resource loadResourceByFileRule(PluginApplicationContext pluginApplicationContext,
|
private Resource loadResourceByFileRule(String pluginId,
|
||||||
|
ApplicationContext pluginApplicationContext,
|
||||||
ReverseProxyRule rule, ServerRequest request) {
|
ReverseProxyRule rule, ServerRequest request) {
|
||||||
Assert.notNull(rule.file(), "File rule must not be null.");
|
Assert.notNull(rule.file(), "File rule must not be null.");
|
||||||
FileReverseProxyProvider file = rule.file();
|
FileReverseProxyProvider file = rule.file();
|
||||||
|
@ -149,7 +141,7 @@ public class ReverseProxyRouterFunctionFactory {
|
||||||
filename = configuredFilename;
|
filename = configuredFilename;
|
||||||
} else {
|
} else {
|
||||||
AntPathMatcher antPathMatcher = new AntPathMatcher();
|
AntPathMatcher antPathMatcher = new AntPathMatcher();
|
||||||
String routePath = buildRoutePath(pluginApplicationContext.getPluginId(), rule);
|
String routePath = buildRoutePath(pluginId, rule);
|
||||||
filename =
|
filename =
|
||||||
antPathMatcher.extractPathWithinPattern(routePath, request.path());
|
antPathMatcher.extractPathWithinPattern(routePath, request.path());
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
package run.halo.app.plugin.resources;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.locks.StampedLock;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.core.extension.ReverseProxy;
|
||||||
|
import run.halo.app.plugin.ExtensionContextRegistry;
|
||||||
|
import run.halo.app.plugin.PluginApplicationContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A registry for {@link RouterFunction} of plugin.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class ReverseProxyRouterFunctionRegistry {
|
||||||
|
private final ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory;
|
||||||
|
private final StampedLock lock = new StampedLock();
|
||||||
|
private final Map<String, RouterFunction<ServerResponse>> proxyNameRouterFunctionRegistry =
|
||||||
|
new HashMap<>();
|
||||||
|
private final MultiValueMap<String, String> pluginIdReverseProxyMap =
|
||||||
|
new LinkedMultiValueMap<>();
|
||||||
|
|
||||||
|
public ReverseProxyRouterFunctionRegistry(
|
||||||
|
ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory) {
|
||||||
|
this.reverseProxyRouterFunctionFactory = reverseProxyRouterFunctionFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register reverse proxy router function.
|
||||||
|
*
|
||||||
|
* @param pluginId plugin id
|
||||||
|
* @param reverseProxy reverse proxy
|
||||||
|
* @return a mono
|
||||||
|
*/
|
||||||
|
public Mono<Void> register(String pluginId, ReverseProxy reverseProxy) {
|
||||||
|
Assert.notNull(pluginId, "The plugin id must not be null.");
|
||||||
|
final String proxyName = reverseProxy.getMetadata().getName();
|
||||||
|
long stamp = lock.writeLock();
|
||||||
|
try {
|
||||||
|
pluginIdReverseProxyMap.add(pluginId, proxyName);
|
||||||
|
// Obtain plugin application context
|
||||||
|
PluginApplicationContext pluginApplicationContext =
|
||||||
|
ExtensionContextRegistry.getInstance().getByPluginId(pluginId);
|
||||||
|
return reverseProxyRouterFunctionFactory.create(reverseProxy, pluginApplicationContext)
|
||||||
|
.map(routerFunction -> {
|
||||||
|
proxyNameRouterFunctionRegistry.put(proxyName, routerFunction);
|
||||||
|
return routerFunction;
|
||||||
|
})
|
||||||
|
.then();
|
||||||
|
} finally {
|
||||||
|
lock.unlockWrite(stamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove reverse proxy router function by plugin id.
|
||||||
|
*
|
||||||
|
* @param pluginId plugin id
|
||||||
|
*/
|
||||||
|
public Mono<Void> remove(String pluginId) {
|
||||||
|
Assert.notNull(pluginId, "The plugin id must not be null.");
|
||||||
|
long stamp = lock.writeLock();
|
||||||
|
try {
|
||||||
|
List<String> proxyNames = pluginIdReverseProxyMap.remove(pluginId);
|
||||||
|
if (proxyNames == null) {
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
for (String proxyName : proxyNames) {
|
||||||
|
proxyNameRouterFunctionRegistry.remove(proxyName);
|
||||||
|
}
|
||||||
|
return Mono.empty();
|
||||||
|
} finally {
|
||||||
|
lock.unlockWrite(stamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove reverse proxy router function by pluginId and reverse proxy name.
|
||||||
|
*/
|
||||||
|
public Mono<Void> remove(String pluginId, String reverseProxyName) {
|
||||||
|
long stamp = lock.writeLock();
|
||||||
|
try {
|
||||||
|
List<String> proxyNames = pluginIdReverseProxyMap.get(pluginId);
|
||||||
|
if (proxyNames == null) {
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
proxyNames.remove(reverseProxyName);
|
||||||
|
proxyNameRouterFunctionRegistry.remove(reverseProxyName);
|
||||||
|
return Mono.empty();
|
||||||
|
} finally {
|
||||||
|
lock.unlockWrite(stamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets reverse proxy {@link RouterFunction} by reverse proxy name.
|
||||||
|
*/
|
||||||
|
public RouterFunction<ServerResponse> getRouterFunction(String proxyName) {
|
||||||
|
long stamp = lock.readLock();
|
||||||
|
try {
|
||||||
|
return proxyNameRouterFunctionRegistry.get(proxyName);
|
||||||
|
} finally {
|
||||||
|
lock.unlockRead(stamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all reverse proxy {@link RouterFunction}.
|
||||||
|
*/
|
||||||
|
public List<RouterFunction<ServerResponse>> getRouterFunctions() {
|
||||||
|
long stamp = lock.readLock();
|
||||||
|
try {
|
||||||
|
return List.copyOf(proxyNameRouterFunctionRegistry.values());
|
||||||
|
} finally {
|
||||||
|
lock.unlockRead(stamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,18 +1,13 @@
|
||||||
package run.halo.app.plugin;
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.util.stream.Stream;
|
import java.util.List;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.pf4j.PluginWrapper;
|
|
||||||
import org.springframework.beans.factory.ObjectProvider;
|
|
||||||
import org.springframework.http.codec.ServerCodecConfigurer;
|
import org.springframework.http.codec.ServerCodecConfigurer;
|
||||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||||
|
@ -23,9 +18,7 @@ import org.springframework.web.reactive.function.server.support.RouterFunctionMa
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
import run.halo.app.plugin.event.HaloPluginStartedEvent;
|
import run.halo.app.plugin.resources.ReverseProxyRouterFunctionRegistry;
|
||||||
import run.halo.app.plugin.event.HaloPluginStoppedEvent;
|
|
||||||
import run.halo.app.plugin.resources.ReverseProxyRouterFunctionFactory;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link PluginCompositeRouterFunction}.
|
* Tests for {@link PluginCompositeRouterFunction}.
|
||||||
|
@ -38,46 +31,27 @@ class PluginCompositeRouterFunctionTest {
|
||||||
private final ServerCodecConfigurer codecConfigurer = ServerCodecConfigurer.create();
|
private final ServerCodecConfigurer codecConfigurer = ServerCodecConfigurer.create();
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory;
|
private ReverseProxyRouterFunctionRegistry reverseProxyRouterFunctionRegistry;
|
||||||
|
|
||||||
@Mock
|
|
||||||
private PluginApplicationContext pluginApplicationContext;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private PluginWrapper pluginWrapper;
|
|
||||||
|
|
||||||
private PluginCompositeRouterFunction compositeRouterFunction;
|
private PluginCompositeRouterFunction compositeRouterFunction;
|
||||||
|
|
||||||
private HandlerFunction<ServerResponse> handlerFunction;
|
private HandlerFunction<ServerResponse> handlerFunction;
|
||||||
private RouterFunction<ServerResponse> routerFunction;
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
void setUp() {
|
void setUp() {
|
||||||
compositeRouterFunction =
|
compositeRouterFunction =
|
||||||
new PluginCompositeRouterFunction(reverseProxyRouterFunctionFactory);
|
new PluginCompositeRouterFunction(reverseProxyRouterFunctionRegistry);
|
||||||
|
|
||||||
ExtensionContextRegistry.getInstance().register("fakeA", pluginApplicationContext);
|
|
||||||
when(pluginWrapper.getPluginId()).thenReturn("fakeA");
|
|
||||||
|
|
||||||
handlerFunction = request -> ServerResponse.ok().build();
|
handlerFunction = request -> ServerResponse.ok().build();
|
||||||
routerFunction = request -> Mono.just(handlerFunction);
|
RouterFunction<ServerResponse> routerFunction = request -> Mono.just(handlerFunction);
|
||||||
|
|
||||||
var objectProvider = mock(ObjectProvider.class);
|
when(reverseProxyRouterFunctionRegistry.getRouterFunctions())
|
||||||
when(objectProvider.orderedStream()).thenReturn(Stream.of(routerFunction));
|
.thenReturn(List.of(routerFunction));
|
||||||
|
|
||||||
when(pluginApplicationContext.getBeanProvider(RouterFunction.class))
|
|
||||||
.thenReturn(objectProvider);
|
|
||||||
when(reverseProxyRouterFunctionFactory.create(any())).thenReturn(Mono.empty());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void route() {
|
void route() {
|
||||||
// trigger haloPluginStartedEvent
|
|
||||||
StepVerifier.create(compositeRouterFunction.onPluginStarted(
|
|
||||||
new HaloPluginStartedEvent(this, pluginWrapper)))
|
|
||||||
.verifyComplete();
|
|
||||||
|
|
||||||
RouterFunctionMapping mapping = new RouterFunctionMapping(compositeRouterFunction);
|
RouterFunctionMapping mapping = new RouterFunctionMapping(compositeRouterFunction);
|
||||||
mapping.setMessageReaders(this.codecConfigurer.getReaders());
|
mapping.setMessageReaders(this.codecConfigurer.getReaders());
|
||||||
|
|
||||||
|
@ -89,30 +63,6 @@ class PluginCompositeRouterFunctionTest {
|
||||||
.verify();
|
.verify();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void onPluginStarted() {
|
|
||||||
assertThat(compositeRouterFunction.getRouterFunction("fakeA")).isNull();
|
|
||||||
|
|
||||||
// trigger haloPluginStartedEvent
|
|
||||||
StepVerifier.create(compositeRouterFunction.onPluginStarted(
|
|
||||||
new HaloPluginStartedEvent(this, pluginWrapper)))
|
|
||||||
.verifyComplete();
|
|
||||||
assertThat(compositeRouterFunction.getRouterFunction("fakeA")).isEqualTo(routerFunction);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void onPluginStopped() {
|
|
||||||
// trigger haloPluginStartedEvent
|
|
||||||
StepVerifier.create(compositeRouterFunction.onPluginStarted(
|
|
||||||
new HaloPluginStartedEvent(this, pluginWrapper)))
|
|
||||||
.verifyComplete();
|
|
||||||
assertThat(compositeRouterFunction.getRouterFunction("fakeA")).isEqualTo(routerFunction);
|
|
||||||
|
|
||||||
// trigger HaloPluginStoppedEvent
|
|
||||||
compositeRouterFunction.onPluginStopped(new HaloPluginStoppedEvent(this, pluginWrapper));
|
|
||||||
assertThat(compositeRouterFunction.getRouterFunction("fakeA")).isNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
private ServerWebExchange createExchange(String urlTemplate) {
|
private ServerWebExchange createExchange(String urlTemplate) {
|
||||||
return MockServerWebExchange.from(MockServerHttpRequest.get(urlTemplate));
|
return MockServerWebExchange.from(MockServerHttpRequest.get(urlTemplate));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package run.halo.app.plugin.resources;
|
package run.halo.app.plugin.resources;
|
||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -11,11 +9,9 @@ import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import reactor.core.publisher.Flux;
|
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
import run.halo.app.core.extension.ReverseProxy;
|
import run.halo.app.core.extension.ReverseProxy;
|
||||||
import run.halo.app.extension.Metadata;
|
import run.halo.app.extension.Metadata;
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
|
||||||
import run.halo.app.plugin.HaloPluginManager;
|
import run.halo.app.plugin.HaloPluginManager;
|
||||||
import run.halo.app.plugin.PluginApplicationContext;
|
import run.halo.app.plugin.PluginApplicationContext;
|
||||||
import run.halo.app.plugin.PluginConst;
|
import run.halo.app.plugin.PluginConst;
|
||||||
|
@ -29,9 +25,6 @@ import run.halo.app.plugin.PluginConst;
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class ReverseProxyRouterFunctionFactoryTest {
|
class ReverseProxyRouterFunctionFactoryTest {
|
||||||
|
|
||||||
@Mock
|
|
||||||
private ReactiveExtensionClient extensionClient;
|
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private PluginApplicationContext pluginApplicationContext;
|
private PluginApplicationContext pluginApplicationContext;
|
||||||
@Mock
|
@Mock
|
||||||
|
@ -42,19 +35,16 @@ class ReverseProxyRouterFunctionFactoryTest {
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
JsBundleRuleProvider jsBundleRuleProvider = new JsBundleRuleProvider(haloPluginManager);
|
JsBundleRuleProvider jsBundleRuleProvider = new JsBundleRuleProvider(haloPluginManager);
|
||||||
reverseProxyRouterFunctionFactory = new ReverseProxyRouterFunctionFactory(extensionClient,
|
reverseProxyRouterFunctionFactory =
|
||||||
jsBundleRuleProvider);
|
new ReverseProxyRouterFunctionFactory(jsBundleRuleProvider);
|
||||||
|
|
||||||
ReverseProxy reverseProxy = mockReverseProxy();
|
|
||||||
|
|
||||||
when(pluginApplicationContext.getPluginId()).thenReturn("fakeA");
|
when(pluginApplicationContext.getPluginId()).thenReturn("fakeA");
|
||||||
when(extensionClient.list(eq(ReverseProxy.class), any(), any())).thenReturn(
|
|
||||||
Flux.just(reverseProxy));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void create() {
|
void create() {
|
||||||
var routerFunction = reverseProxyRouterFunctionFactory.create(pluginApplicationContext);
|
var routerFunction =
|
||||||
|
reverseProxyRouterFunctionFactory.create(mockReverseProxy(), pluginApplicationContext);
|
||||||
StepVerifier.create(routerFunction)
|
StepVerifier.create(routerFunction)
|
||||||
.expectNextCount(1)
|
.expectNextCount(1)
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
|
|
Loading…
Reference in New Issue