Check dependencies while installing and upgrading plugins (#5361)

#### What type of PR is this?

/kind improvement
/area core
/milestone 2.13.x

#### What this PR does / why we need it:

This PR checks dependencies while installing and upgrading plugins.

Steps to test:

1. Prepare a plugin with dependencies.

	```diff
	apiVersion: plugin.halo.run/v1alpha1
	kind: Plugin
	metadata:
	  name: a-plugin
	...
	+  pluginDependencies:
	+    app-store-integration: 1.*
	```
2. Build the plugin.
3. Try to uninstall `app-store` plugin.
4. Install `a-plugin`.
5. See the result

#### Which issue(s) this PR fixes:

Fixes #5345 

#### Does this PR introduce a user-facing change?

```release-note
插件安装和更新时检查依赖是否合法
```
pull/5422/head^2
John Niang 2024-02-27 18:51:13 +08:00 committed by GitHub
parent 827030dd68
commit d2f569699b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 117 additions and 11 deletions

View File

@ -10,6 +10,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
@ -17,6 +18,9 @@ import java.util.Objects;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.pf4j.DependencyResolver;
import org.pf4j.PluginDescriptor;
import org.pf4j.PluginManager;
import org.pf4j.PluginWrapper;
import org.pf4j.RuntimeMode;
import org.springframework.core.io.Resource;
@ -37,14 +41,15 @@ import run.halo.app.core.extension.service.PluginService;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.infra.SystemVersionSupplier;
import run.halo.app.infra.exception.PluginAlreadyExistsException;
import run.halo.app.infra.exception.PluginDependencyException;
import run.halo.app.infra.exception.PluginInstallationException;
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.PluginConst;
import run.halo.app.plugin.PluginProperties;
import run.halo.app.plugin.PluginUtils;
import run.halo.app.plugin.YamlPluginDescriptorFinder;
import run.halo.app.plugin.YamlPluginFinder;
import run.halo.app.plugin.resources.BundleResourceUtils;
@ -62,7 +67,7 @@ public class PluginServiceImpl implements PluginService {
private final PluginProperties pluginProperties;
private final HaloPluginManager pluginManager;
private final PluginManager pluginManager;
@Override
public Flux<Plugin> getPresets() {
@ -82,11 +87,13 @@ public class PluginServiceImpl implements PluginService {
@Override
public Mono<Plugin> install(Path path) {
return findPluginManifest(path)
.flatMap(pluginInPath -> {
.doOnNext(plugin -> {
// validate the plugin version
satisfiesRequiresVersion(pluginInPath);
return client.fetch(Plugin.class, pluginInPath.getMetadata().getName())
satisfiesRequiresVersion(plugin);
checkDependencies(plugin);
})
.flatMap(pluginInPath ->
client.fetch(Plugin.class, pluginInPath.getMetadata().getName())
.flatMap(oldPlugin -> Mono.<Plugin>error(
new PluginAlreadyExistsException(oldPlugin.getMetadata().getName())))
.switchIfEmpty(Mono.defer(
@ -97,18 +104,47 @@ public class PluginServiceImpl implements PluginService {
p.getSpec().setEnabled(false);
})
.flatMap(client::create))
);
});
));
}
private void checkDependencies(Plugin plugin) {
var resolvedPlugins = new ArrayList<>(pluginManager.getResolvedPlugins());
var pluginDescriptors = new ArrayList<PluginDescriptor>(resolvedPlugins.size() + 1);
resolvedPlugins.stream()
.map(PluginWrapper::getDescriptor)
.forEach(pluginDescriptors::add);
var pluginDescriptor = YamlPluginDescriptorFinder.convert(plugin);
pluginDescriptors.add(pluginDescriptor);
var deptResolver = new DependencyResolver(pluginManager.getVersionManager());
var result = deptResolver.resolve(pluginDescriptors);
if (result.hasCyclicDependency()) {
throw new PluginDependencyException.CyclicException();
}
var notFoundDependencies = result.getNotFoundDependencies();
if (!CollectionUtils.isEmpty(notFoundDependencies)) {
throw new PluginDependencyException.NotFoundException(notFoundDependencies);
}
var wrongVersionDependencies = result.getWrongVersionDependencies();
if (!CollectionUtils.isEmpty(wrongVersionDependencies)) {
throw new PluginDependencyException.WrongVersionsException(wrongVersionDependencies);
}
}
@Override
public Mono<Plugin> upgrade(String name, Path path) {
return findPluginManifest(path)
.doOnNext(plugin -> {
satisfiesRequiresVersion(plugin);
checkDependencies(plugin);
})
.flatMap(pluginInPath -> {
// pre-check the plugin in the path
Assert.notNull(pluginInPath.statusNonNull().getLoadLocation(),
"plugin.status.load-location must not be null");
satisfiesRequiresVersion(pluginInPath);
if (!Objects.equals(name, pluginInPath.getMetadata().getName())) {
return Mono.error(new ServerWebInputException(
"The provided plugin " + pluginInPath.getMetadata().getName()

View File

@ -0,0 +1,56 @@
package run.halo.app.infra.exception;
import java.net.URI;
import java.util.List;
import org.pf4j.DependencyResolver.WrongDependencyVersion;
import org.springframework.web.server.ServerWebInputException;
public abstract class PluginDependencyException extends ServerWebInputException {
public PluginDependencyException(String reason) {
super(reason);
}
public PluginDependencyException(String reason, Throwable cause) {
super(reason, null, cause);
}
protected PluginDependencyException(String reason, Throwable cause,
String messageDetailCode, Object[] messageDetailArguments) {
super(reason, null, cause, messageDetailCode, messageDetailArguments);
}
public static class CyclicException extends PluginDependencyException {
public static final String TYPE = "https://halo.run/probs/plugin-cyclic-dependency";
public CyclicException() {
super("A cyclic dependency was detected.");
setType(URI.create(TYPE));
}
}
public static class NotFoundException extends PluginDependencyException {
public static final String TYPE = "https://halo.run/probs/plugin-dependencies-not-found";
public NotFoundException(List<String> dependencies) {
super("Dependencies were not found.", null, null, new Object[] {dependencies});
setType(URI.create(TYPE));
getBody().setProperty("dependencies", dependencies);
}
}
public static class WrongVersionsException extends PluginDependencyException {
public static final String TYPE =
"https://halo.run/probs/plugin-dependencies-with-wrong-versions";
public WrongVersionsException(List<WrongDependencyVersion> versions) {
super("Dependencies have wrong version.", null, null, new Object[] {versions});
setType(URI.create(TYPE));
getBody().setProperty("versions", versions);
}
}
}

View File

@ -43,7 +43,7 @@ public class YamlPluginDescriptorFinder implements PluginDescriptorFinder {
return convert(plugin);
}
private DefaultPluginDescriptor convert(Plugin plugin) {
public static PluginDescriptor convert(Plugin plugin) {
String pluginId = plugin.getMetadata().getName();
Plugin.PluginSpec spec = plugin.getSpec();
Plugin.PluginAuthor author = spec.getAuthor();
@ -66,7 +66,7 @@ public class YamlPluginDescriptorFinder implements PluginDescriptorFinder {
return defaultPluginDescriptor;
}
private String joinLicense(List<Plugin.License> licenses) {
private static String joinLicense(List<Plugin.License> licenses) {
if (CollectionUtils.isEmpty(licenses)) {
return StringUtils.EMPTY;
}

View File

@ -21,6 +21,9 @@ problemDetail.title.run.halo.app.infra.exception.DuplicateNameException=Duplicat
problemDetail.title.run.halo.app.infra.exception.RateLimitExceededException=Request Not Permitted
problemDetail.title.run.halo.app.infra.exception.NotFoundException=Resource Not Found
problemDetail.title.run.halo.app.infra.exception.EmailVerificationFailed=Email Verification Failed
problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$CyclicException=Cyclic Dependency Detected
problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$NotFoundException=Dependencies Not Found
problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=Wrong Dependency Version
problemDetail.title.internalServerError=Internal Server Error
# Detail definitions
@ -40,6 +43,10 @@ problemDetail.run.halo.app.infra.exception.DuplicateNameException=Duplicate name
problemDetail.run.halo.app.infra.exception.PluginAlreadyExistsException=Plugin {0} already exists.
problemDetail.run.halo.app.infra.exception.RateLimitExceededException=API rate limit exceeded, please try again later.
problemDetail.run.halo.app.infra.exception.EmailVerificationFailed=Invalid email verification code.
problemDetail.run.halo.app.infra.exception.PluginDependencyException$CyclicException=A cyclic dependency was detected.
problemDetail.run.halo.app.infra.exception.PluginDependencyException$NotFoundException=Dependencies "{0}" were not found.
problemDetail.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=Dependencies have wrong version: {0}.
problemDetail.index.duplicateKey=The value of {0} already exists for unique index {1}, please rename it and retry.
problemDetail.user.email.verify.maxAttempts=Too many verification attempts, please try again later.
problemDetail.user.password.unsatisfied=The password does not meet the specifications.

View File

@ -9,6 +9,9 @@ problemDetail.title.run.halo.app.infra.exception.ThemeInstallationException=主
problemDetail.title.run.halo.app.infra.exception.RateLimitExceededException=请求限制
problemDetail.title.run.halo.app.infra.exception.NotFoundException=资源不存在
problemDetail.title.run.halo.app.infra.exception.EmailVerificationFailed=邮箱验证失败
problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$CyclicException=循环依赖
problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$NotFoundException=依赖未找到
problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=依赖版本错误
problemDetail.title.internalServerError=服务器内部错误
problemDetail.org.springframework.security.authentication.BadCredentialsException=用户名或密码错误。
@ -17,6 +20,10 @@ problemDetail.run.halo.app.infra.exception.DuplicateNameException=检测到有
problemDetail.run.halo.app.infra.exception.PluginAlreadyExistsException=插件 {0} 已经存在。
problemDetail.run.halo.app.infra.exception.RateLimitExceededException=请求过于频繁,请稍候再试。
problemDetail.run.halo.app.infra.exception.EmailVerificationFailed=验证码错误或已失效。
problemDetail.run.halo.app.infra.exception.PluginDependencyException$CyclicException=检测到循环依赖。
problemDetail.run.halo.app.infra.exception.PluginDependencyException$NotFoundException=依赖“{0}”未找到。
problemDetail.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=依赖版本有误:{0}。
problemDetail.index.duplicateKey=唯一索引 {1} 中的值 {0} 已存在,请更名后重试。
problemDetail.user.email.verify.maxAttempts=尝试次数过多,请稍候再试。
problemDetail.user.password.unsatisfied=密码不符合规范。