Skip to content

Commit

Permalink
feat: create reverse proxy for logo when plugin created (#2652)
Browse files Browse the repository at this point in the history
#### What type of PR is this?
/kind improvement
/area core
/milestone 2.0
#### What this PR does / why we need it:
插件 logo 支持配置外部 URL 或相对于 resources 的路径

Console 端展示插件 logo 时使用的字段需要使用 status.logo 而非 spec.logo
#### Which issue(s) this PR fixes:
Fixes #2651

#### Special notes for your reviewer:
how to test it?
1. 插件配置 logo 为外部 url 例如 https://guqing.xyz/avatar
2. 安装此插件后不会注册 ReverseProxy 规则
3. logo 配置为相对于 resources 的路径例如:/logo.png
4. 安装此插件后可以访问到 /plugins/{your-plugin-name}/assets/logo.png
5. 开发模式启动插件后,修改了 plugin.yaml 中的 spec.logo 则也会更新 ReverseProxy 的 rule

/cc @halo-dev/sig-halo 
#### Does this PR introduce a user-facing change?
```release-note
插件 Logo 支持配置外部 URL 或相对于 resources 的路径
```
  • Loading branch information
guqing authored Nov 3, 2022
1 parent 1078145 commit 73df5e4
Show file tree
Hide file tree
Showing 9 changed files with 269 additions and 49 deletions.
2 changes: 2 additions & 0 deletions src/main/java/run/halo/app/core/extension/Plugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ public static class PluginStatus {
private String entry;

private String stylesheet;

private String logo;
}

@JsonIgnore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,29 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.pf4j.PluginRuntimeException;
import org.pf4j.PluginState;
import org.pf4j.PluginWrapper;
import org.pf4j.RuntimeMode;
import run.halo.app.core.extension.Plugin;
import run.halo.app.core.extension.ReverseProxy;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.controller.Reconciler;
import run.halo.app.extension.controller.Reconciler.Request;
import run.halo.app.infra.utils.JsonUtils;
import run.halo.app.infra.utils.PathUtils;
import run.halo.app.plugin.HaloPluginManager;
import run.halo.app.plugin.PluginConst;
import run.halo.app.plugin.PluginStartingError;
import run.halo.app.plugin.resources.BundleResourceUtils;

Expand Down Expand Up @@ -51,6 +58,7 @@ public Result reconcile(Request request) {
}
addFinalizerIfNecessary(plugin);
reconcilePluginState(plugin.getMetadata().getName());
createInitialReverseProxyIfNotPresent(plugin);
});
return new Result(false, null);
}
Expand Down Expand Up @@ -78,6 +86,15 @@ private void reconcilePluginState(String name) {
}
}

String logo = plugin.getSpec().getLogo();
if (PathUtils.isAbsoluteUri(logo)) {
pluginStatus.setLogo(logo);
} else {
String assetsPrefix =
PluginConst.assertsRoutePrefix(plugin.getMetadata().getName());
pluginStatus.setLogo(PathUtils.combinePath(assetsPrefix, logo));
}

if (!plugin.equals(oldPlugin)) {
client.update(plugin);
}
Expand Down Expand Up @@ -213,4 +230,45 @@ private void cleanUpResources(Plugin plugin) {
}
}
}

void createInitialReverseProxyIfNotPresent(Plugin plugin) {
String pluginName = plugin.getMetadata().getName();
String reverseProxyName = initialReverseProxyName(pluginName);
ReverseProxy reverseProxy = new ReverseProxy();
reverseProxy.setMetadata(new Metadata());
reverseProxy.getMetadata().setName(reverseProxyName);
// put label to identify this reverse
reverseProxy.getMetadata().setLabels(new HashMap<>());
reverseProxy.getMetadata().getLabels().put(PluginConst.PLUGIN_NAME_LABEL_NAME, pluginName);

reverseProxy.setRules(new ArrayList<>());

String logo = plugin.getSpec().getLogo();
if (StringUtils.isNotBlank(logo) && !PathUtils.isAbsoluteUri(logo)) {
ReverseProxy.ReverseProxyRule logoRule = new ReverseProxy.ReverseProxyRule(logo,
new ReverseProxy.FileReverseProxyProvider(null, logo));
reverseProxy.getRules().add(logoRule);
}

client.fetch(ReverseProxy.class, reverseProxyName)
.ifPresentOrElse(persisted -> {
if (isDevelopmentMode(pluginName)) {
reverseProxy.getMetadata()
.setVersion(persisted.getMetadata().getVersion());
client.update(reverseProxy);
}
}, () -> client.create(reverseProxy));
}

static String initialReverseProxyName(String pluginName) {
return pluginName + "-system-generated-reverse-proxy";
}

private boolean isDevelopmentMode(String name) {
PluginWrapper pluginWrapper = haloPluginManager.getPlugin(name);
if (pluginWrapper == null) {
return false;
}
return RuntimeMode.DEVELOPMENT.equals(pluginWrapper.getRuntimeMode());
}
}
34 changes: 34 additions & 0 deletions src/main/java/run/halo/app/infra/utils/PathUtils.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package run.halo.app.infra.utils;

import java.net.URI;
import java.net.URISyntaxException;
import lombok.experimental.UtilityClass;
import org.apache.commons.lang3.StringUtils;

Expand All @@ -12,6 +14,38 @@
@UtilityClass
public class PathUtils {

/**
* Every HTTP URL conforms to the syntax of a generic URI. The URI generic syntax consists of
* components organized hierarchically in order of decreasing significance from left to
* right:
* <pre>
* URI = scheme ":" ["//" authority] path ["?" query] ["#" fragment]
* </pre>
* The authority component consists of subcomponents:
* <pre>
* authority = [userinfo "@"] host [":" port]
* </pre>
* Examples of popular schemes include http, https, ftp, mailto, file, data and irc. URI
* schemes should be registered with the
* <a href="https://en.wikipedia.org/wiki/Internet_Assigned_Numbers_Authority">Internet Assigned Numbers Authority (IANA)</a>, although
* non-registered schemes are used in practice.
*
* @param uriString url or path
* @return true if the linkBase is absolute, otherwise false
* @see <a href="https://en.wikipedia.org/wiki/URL">URL</a>
*/
public static boolean isAbsoluteUri(final String uriString) {
if (StringUtils.isBlank(uriString)) {
return false;
}
try {
URI uri = new URI(uriString);
return uri.isAbsolute();
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}

/**
* Combine paths based on the passed in path segments parameters.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public static Resource getJsBundleResource(HaloPluginManager pluginManager, Stri
}

@Nullable
private static DefaultResourceLoader getResourceLoader(HaloPluginManager pluginManager,
public static DefaultResourceLoader getResourceLoader(HaloPluginManager pluginManager,
String pluginName) {
Assert.notNull(pluginManager, "Plugin manager must not be null");
PluginWrapper plugin = pluginManager.getPlugin(pluginName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;

import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.http.server.PathContainer;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
Expand All @@ -22,8 +25,10 @@
import run.halo.app.core.extension.ReverseProxy;
import run.halo.app.core.extension.ReverseProxy.FileReverseProxyProvider;
import run.halo.app.core.extension.ReverseProxy.ReverseProxyRule;
import run.halo.app.infra.exception.NotFoundException;
import run.halo.app.infra.utils.PathUtils;
import run.halo.app.plugin.PluginApplicationContext;
import run.halo.app.plugin.ExtensionContextRegistry;
import run.halo.app.plugin.HaloPluginManager;
import run.halo.app.plugin.PluginConst;

/**
Expand All @@ -36,39 +41,40 @@
*/
@Slf4j
@Component
@AllArgsConstructor
public class ReverseProxyRouterFunctionFactory {

private final HaloPluginManager haloPluginManager;
private final ApplicationContext applicationContext;

/**
* <p>Create {@link RouterFunction} according to the {@link ReverseProxy} custom resource
* configuration of the plugin.</p>
* <p>Note that: returns {@code Null} if the plugin does not have a {@link ReverseProxy} custom
* resource.</p>
*
* @param applicationContext plugin application context or system application context
* @param pluginName plugin name(nullable if system)
* @return A reverse proxy RouterFunction handle(nullable)
*/
@NonNull
public Mono<RouterFunction<ServerResponse>> create(ReverseProxy reverseProxy,
ApplicationContext applicationContext) {
return createReverseProxyRouterFunction(reverseProxy, applicationContext);
String pluginName) {
return createReverseProxyRouterFunction(reverseProxy, nullSafePluginName(pluginName));
}

private Mono<RouterFunction<ServerResponse>> createReverseProxyRouterFunction(
ReverseProxy reverseProxy,
ApplicationContext applicationContext) {
ReverseProxy reverseProxy, @NonNull String pluginName) {
Assert.notNull(reverseProxy, "The reverseProxy must not be null.");
Assert.notNull(applicationContext, "The applicationContext must not be null.");
final var pluginId = getPluginId(applicationContext);
var rules = getReverseProxyRules(reverseProxy);

return rules.map(rule -> {
String routePath = buildRoutePath(pluginId, rule);
log.debug("Plugin [{}] registered reverse proxy route path [{}]", pluginId,
String routePath = buildRoutePath(pluginName, rule);
log.debug("Plugin [{}] registered reverse proxy route path [{}]", pluginName,
routePath);
return RouterFunctions.route(GET(routePath).and(accept(ALL)),
request -> {
Resource resource =
loadResourceByFileRule(pluginId, applicationContext, rule, request);
loadResourceByFileRule(pluginName, rule, request);
if (!resource.exists()) {
return ServerResponse.notFound().build();
}
Expand All @@ -78,11 +84,8 @@ private Mono<RouterFunction<ServerResponse>> createReverseProxyRouterFunction(
}).reduce(RouterFunction::and);
}

private String getPluginId(ApplicationContext applicationContext) {
if (applicationContext instanceof PluginApplicationContext pluginApplicationContext) {
return pluginApplicationContext.getPluginId();
}
return PluginConst.SYSTEM_PLUGIN_NAME;
private String nullSafePluginName(String pluginName) {
return pluginName == null ? PluginConst.SYSTEM_PLUGIN_NAME : pluginName;
}

private Flux<ReverseProxyRule> getReverseProxyRules(ReverseProxy reverseProxy) {
Expand All @@ -105,15 +108,14 @@ public static String buildRoutePath(String pluginId, ReverseProxyRule reversePro
* <p>Note that a returned Resource handle does not imply an existing resource; you need to
* invoke {@link Resource#exists()} to check for existence</p>
*
* @param pluginApplicationContext load file from plugin
* @param pluginName plugin to load file by name
* @param rule reverse proxy rule
* @param request client request
* @return a Resource handle for the specified resource location by the plugin(never null);
*/
@NonNull
private Resource loadResourceByFileRule(String pluginId,
ApplicationContext pluginApplicationContext,
ReverseProxyRule rule, ServerRequest request) {
private Resource loadResourceByFileRule(String pluginName, ReverseProxyRule rule,
ServerRequest request) {
Assert.notNull(rule.file(), "File rule must not be null.");
FileReverseProxyProvider file = rule.file();
String directory = file.directory();
Expand All @@ -124,14 +126,30 @@ private Resource loadResourceByFileRule(String pluginId,
if (StringUtils.isNotBlank(configuredFilename)) {
filename = configuredFilename;
} else {
String routePath = buildRoutePath(pluginId, rule);
String routePath = buildRoutePath(pluginName, rule);
PathContainer pathContainer = PathPatternParser.defaultInstance.parse(routePath)
.extractPathWithinPattern(PathContainer.parsePath(request.path()));
filename = pathContainer.value();
}

String filePath = PathUtils.combinePath(directory, filename);
return pluginApplicationContext.getResource(filePath);
return getResourceLoader(pluginName).getResource(filePath);
}

private ResourceLoader getResourceLoader(String pluginName) {
ExtensionContextRegistry registry = ExtensionContextRegistry.getInstance();
if (registry.containsContext(pluginName)) {
return registry.getByPluginId(pluginName);
}
if (PluginConst.SYSTEM_PLUGIN_NAME.equals(pluginName)) {
return applicationContext;
}
DefaultResourceLoader resourceLoader =
BundleResourceUtils.getResourceLoader(haloPluginManager, pluginName);
if (resourceLoader == null) {
throw new NotFoundException("Plugin [" + pluginName + "] not found.");
}
return resourceLoader;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
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.
Expand Down Expand Up @@ -48,11 +46,7 @@ public Mono<Void> register(String pluginId, ReverseProxy reverseProxy) {
long stamp = lock.writeLock();
try {
pluginIdReverseProxyMap.put(pluginId, proxyName);

// Obtain plugin application context
PluginApplicationContext pluginApplicationContext =
ExtensionContextRegistry.getInstance().getByPluginId(pluginId);
return reverseProxyRouterFunctionFactory.create(reverseProxy, pluginApplicationContext)
return reverseProxyRouterFunctionFactory.create(reverseProxy, pluginId)
.map(routerFunction -> {
proxyNameRouterFunctionRegistry.put(proxyName, routerFunction);
return routerFunction;
Expand Down
Loading

0 comments on commit 73df5e4

Please sign in to comment.