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
guqing 2022-10-09 11:42:30 +08:00 committed by GitHub
parent a4609f68d1
commit 04300308fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 289 additions and 161 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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