mirror of https://github.com/halo-dev/halo
feat: add request mapping handler mapping for plugin (#2161)
* feat: add requet mapping handler mapping for plugin * fix: request info resolve * fix: test code style * refactor: plugin api version resolve * fix: merge conflictspull/2175/head
parent
89eeccd99c
commit
7cd1282ad3
|
@ -4,7 +4,9 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import run.halo.app.extension.AbstractExtension;
|
import run.halo.app.extension.AbstractExtension;
|
||||||
import run.halo.app.extension.GVK;
|
import run.halo.app.extension.GVK;
|
||||||
|
|
||||||
|
@ -32,29 +34,28 @@ public class Role extends AbstractExtension {
|
||||||
* @author guqing
|
* @author guqing
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
@Data
|
@Getter
|
||||||
public static class PolicyRule {
|
public static class PolicyRule {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* APIGroups is the name of the APIGroup that contains the resources.
|
* APIGroups is the name of the APIGroup that contains the resources.
|
||||||
* If multiple API groups are specified, any action requested against one of the enumerated
|
* If multiple API groups are specified, any action requested against one of the enumerated
|
||||||
* resources in any API group will be allowed.
|
* resources in any API group will be allowed.
|
||||||
*/
|
*/
|
||||||
String[] apiGroups;
|
final String[] apiGroups;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resources is a list of resources this rule applies to. '*' represents all resources in
|
* Resources is a list of resources this rule applies to. '*' represents all resources in
|
||||||
* the specified apiGroups.
|
* the specified apiGroups.
|
||||||
* '*/foo' represents the subresource 'foo' for all resources in
|
* '*/foo' represents the subresource 'foo' for all resources in the specified
|
||||||
* the specified apiGroups.
|
* apiGroups.
|
||||||
*/
|
*/
|
||||||
String[] resources;
|
final String[] resources;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ResourceNames is an optional white list of names that the rule applies to. An empty set
|
* ResourceNames is an optional white list of names that the rule applies to. An empty set
|
||||||
* means that everything is allowed.
|
* means that everything is allowed.
|
||||||
*/
|
*/
|
||||||
String[] resourceNames;
|
final String[] resourceNames;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NonResourceURLs is a set of partial urls that a user should have access to.
|
* NonResourceURLs is a set of partial urls that a user should have access to.
|
||||||
|
@ -66,19 +67,26 @@ public class Role extends AbstractExtension {
|
||||||
* Rules can either apply to API resources (such as "pods" or "secrets") or non-resource
|
* Rules can either apply to API resources (such as "pods" or "secrets") or non-resource
|
||||||
* URL paths (such as "/api"), but not both.
|
* URL paths (such as "/api"), but not both.
|
||||||
*/
|
*/
|
||||||
String[] nonResourceURLs;
|
final String[] nonResourceURLs;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* about who the rule applies to or which namespace the rule applies to.
|
* about who the rule applies to or which namespace the rule applies to.
|
||||||
*/
|
*/
|
||||||
String[] verbs;
|
final String[] verbs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the plugin name exists, it means that the API is provided by the plugin.
|
||||||
|
*/
|
||||||
|
final String pluginName;
|
||||||
|
|
||||||
public PolicyRule() {
|
public PolicyRule() {
|
||||||
this(null, null, null, null, null);
|
this(null, null, null, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PolicyRule(String[] apiGroups, String[] resources, String[] resourceNames,
|
public PolicyRule(String pluginName, String[] apiGroups, String[] resources,
|
||||||
|
String[] resourceNames,
|
||||||
String[] nonResourceURLs, String[] verbs) {
|
String[] nonResourceURLs, String[] verbs) {
|
||||||
|
this.pluginName = StringUtils.defaultString(pluginName);
|
||||||
this.apiGroups = nullElseEmpty(apiGroups);
|
this.apiGroups = nullElseEmpty(apiGroups);
|
||||||
this.resources = nullElseEmpty(resources);
|
this.resources = nullElseEmpty(resources);
|
||||||
this.resourceNames = nullElseEmpty(resourceNames);
|
this.resourceNames = nullElseEmpty(resourceNames);
|
||||||
|
@ -95,11 +103,22 @@ public class Role extends AbstractExtension {
|
||||||
|
|
||||||
public static class Builder {
|
public static class Builder {
|
||||||
String[] apiGroups;
|
String[] apiGroups;
|
||||||
|
|
||||||
String[] resources;
|
String[] resources;
|
||||||
|
|
||||||
String[] resourceNames;
|
String[] resourceNames;
|
||||||
|
|
||||||
String[] nonResourceURLs;
|
String[] nonResourceURLs;
|
||||||
|
|
||||||
String[] verbs;
|
String[] verbs;
|
||||||
|
|
||||||
|
String pluginName;
|
||||||
|
|
||||||
|
public Builder pluginName(String pluginName) {
|
||||||
|
this.pluginName = pluginName;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Builder apiGroups(String... apiGroups) {
|
public Builder apiGroups(String... apiGroups) {
|
||||||
this.apiGroups = apiGroups;
|
this.apiGroups = apiGroups;
|
||||||
return this;
|
return this;
|
||||||
|
@ -126,7 +145,9 @@ public class Role extends AbstractExtension {
|
||||||
}
|
}
|
||||||
|
|
||||||
public PolicyRule build() {
|
public PolicyRule build() {
|
||||||
return new PolicyRule(apiGroups, resources, resourceNames, nonResourceURLs, verbs);
|
return new PolicyRule(pluginName, apiGroups, resources, resourceNames,
|
||||||
|
nonResourceURLs,
|
||||||
|
verbs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Api version.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@Target({ElementType.TYPE})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
public @interface ApiVersion {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Api version value.
|
||||||
|
*
|
||||||
|
* @return api version string
|
||||||
|
*/
|
||||||
|
String value();
|
||||||
|
}
|
|
@ -183,7 +183,7 @@ public class HaloPluginManager extends DefaultPluginManager
|
||||||
|
|
||||||
pluginWrapper.getPlugin().start();
|
pluginWrapper.getPlugin().start();
|
||||||
|
|
||||||
requestMappingManager.registerControllers(pluginWrapper);
|
requestMappingManager.registerHandlerMappings(pluginWrapper);
|
||||||
|
|
||||||
pluginWrapper.setPluginState(PluginState.STARTED);
|
pluginWrapper.setPluginState(PluginState.STARTED);
|
||||||
pluginWrapper.setFailedException(null);
|
pluginWrapper.setFailedException(null);
|
||||||
|
@ -259,7 +259,7 @@ public class HaloPluginManager extends DefaultPluginManager
|
||||||
// create plugin instance and start it
|
// create plugin instance and start it
|
||||||
pluginWrapper.getPlugin().start();
|
pluginWrapper.getPlugin().start();
|
||||||
|
|
||||||
requestMappingManager.registerControllers(pluginWrapper);
|
requestMappingManager.registerHandlerMappings(pluginWrapper);
|
||||||
|
|
||||||
pluginWrapper.setPluginState(PluginState.STARTED);
|
pluginWrapper.setPluginState(PluginState.STARTED);
|
||||||
startedPlugins.add(pluginWrapper);
|
startedPlugins.add(pluginWrapper);
|
||||||
|
@ -358,7 +358,7 @@ public class HaloPluginManager extends DefaultPluginManager
|
||||||
*/
|
*/
|
||||||
public void releaseAdditionalResources(String pluginId) {
|
public void releaseAdditionalResources(String pluginId) {
|
||||||
// release request mapping
|
// release request mapping
|
||||||
requestMappingManager.removeControllerMapping(pluginId);
|
requestMappingManager.removeHandlerMappings(pluginId);
|
||||||
try {
|
try {
|
||||||
pluginApplicationInitializer.contextDestroyed(pluginId);
|
pluginApplicationInitializer.contextDestroyed(pluginId);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
|
@ -14,11 +14,12 @@ import org.pf4j.PluginLoader;
|
||||||
import org.pf4j.PluginManager;
|
import org.pf4j.PluginManager;
|
||||||
import org.pf4j.PluginStatusProvider;
|
import org.pf4j.PluginStatusProvider;
|
||||||
import org.pf4j.RuntimeMode;
|
import org.pf4j.RuntimeMode;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
|
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugin autoconfiguration for Spring Boot.
|
* Plugin autoconfiguration for Spring Boot.
|
||||||
|
@ -32,18 +33,26 @@ import org.springframework.web.reactive.result.method.annotation.RequestMappingH
|
||||||
public class PluginAutoConfiguration {
|
public class PluginAutoConfiguration {
|
||||||
|
|
||||||
private final PluginProperties pluginProperties;
|
private final PluginProperties pluginProperties;
|
||||||
|
@Qualifier("webFluxContentTypeResolver")
|
||||||
private final RequestMappingHandlerMapping requestMappingHandlerMapping;
|
private final RequestedContentTypeResolver requestedContentTypeResolver;
|
||||||
|
|
||||||
public PluginAutoConfiguration(PluginProperties pluginProperties,
|
public PluginAutoConfiguration(PluginProperties pluginProperties,
|
||||||
RequestMappingHandlerMapping requestMappingHandlerMapping) {
|
RequestedContentTypeResolver requestedContentTypeResolver) {
|
||||||
this.pluginProperties = pluginProperties;
|
this.pluginProperties = pluginProperties;
|
||||||
this.requestMappingHandlerMapping = requestMappingHandlerMapping;
|
this.requestedContentTypeResolver = requestedContentTypeResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PluginRequestMappingHandlerMapping pluginRequestMappingHandlerMapping() {
|
||||||
|
PluginRequestMappingHandlerMapping mapping = new PluginRequestMappingHandlerMapping();
|
||||||
|
mapping.setContentTypeResolver(requestedContentTypeResolver);
|
||||||
|
mapping.setOrder(-1);
|
||||||
|
return mapping;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public PluginRequestMappingManager pluginRequestMappingManager() {
|
public PluginRequestMappingManager pluginRequestMappingManager() {
|
||||||
return new PluginRequestMappingManager(requestMappingHandlerMapping);
|
return new PluginRequestMappingManager(pluginRequestMappingHandlerMapping());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|
|
@ -98,6 +98,9 @@ public class PluginCompositeRouterFunction implements RouterFunction<ServerRespo
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private List<RouterFunction<ServerResponse>> routerFunctions(
|
private List<RouterFunction<ServerResponse>> routerFunctions(
|
||||||
PluginApplicationContext applicationContext) {
|
PluginApplicationContext applicationContext) {
|
||||||
|
// TODO: Since the parent of the ApplicationContext of the plugin is RootApplicationContext
|
||||||
|
// obtaining the RouterFunction here will obtain the existing in the parent
|
||||||
|
// resulting in a loop when there is no matching route
|
||||||
List<RouterFunction<ServerResponse>> functions =
|
List<RouterFunction<ServerResponse>> functions =
|
||||||
applicationContext.getBeanProvider(RouterFunction.class)
|
applicationContext.getBeanProvider(RouterFunction.class)
|
||||||
.orderedStream()
|
.orderedStream()
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.pf4j.PluginRuntimeException;
|
||||||
|
import org.springframework.aop.support.AopUtils;
|
||||||
|
import org.springframework.core.MethodIntrospector;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.reactive.result.method.RequestMappingInfo;
|
||||||
|
import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping;
|
||||||
|
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
|
||||||
|
import run.halo.app.extension.GroupVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An extension of {@link RequestMappingInfoHandlerMapping} that creates
|
||||||
|
* {@link RequestMappingInfo} instances from class-level and method-level
|
||||||
|
* {@link RequestMapping} annotations used by plugin.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
public class PluginRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
|
||||||
|
|
||||||
|
private final MultiValueMap<String, RequestMappingInfo> pluginMappingInfo =
|
||||||
|
new LinkedMultiValueMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register handler methods according to the plugin id and the controller(annotated
|
||||||
|
* {@link Controller}) bean.
|
||||||
|
*
|
||||||
|
* @param pluginId plugin id to be registered
|
||||||
|
* @param handler controller bean
|
||||||
|
*/
|
||||||
|
public void registerHandlerMethods(String pluginId, Object handler) {
|
||||||
|
Class<?> handlerType = (handler instanceof String beanName
|
||||||
|
? obtainApplicationContext().getType(beanName) : handler.getClass());
|
||||||
|
|
||||||
|
if (handlerType != null) {
|
||||||
|
final Class<?> userType = ClassUtils.getUserClass(handlerType);
|
||||||
|
Map<Method, RequestMappingInfo> methods = MethodIntrospector.selectMethods(userType,
|
||||||
|
(MethodIntrospector.MetadataLookup<RequestMappingInfo>)
|
||||||
|
method -> getPluginMappingForMethod(pluginId, method, userType));
|
||||||
|
if (logger.isTraceEnabled()) {
|
||||||
|
logger.trace(formatMappings(userType, methods));
|
||||||
|
} else if (mappingsLogger.isDebugEnabled()) {
|
||||||
|
mappingsLogger.debug(formatMappings(userType, methods));
|
||||||
|
}
|
||||||
|
methods.forEach((method, mapping) -> {
|
||||||
|
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
|
||||||
|
registerHandlerMethod(handler, invocableMethod, mapping);
|
||||||
|
pluginMappingInfo.add(pluginId, mapping);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatMappings(Class<?> userType, Map<Method, RequestMappingInfo> methods) {
|
||||||
|
String packageName = ClassUtils.getPackageName(userType);
|
||||||
|
String formattedType = (StringUtils.hasText(packageName)
|
||||||
|
? Arrays.stream(packageName.split("\\."))
|
||||||
|
.map(packageSegment -> packageSegment.substring(0, 1))
|
||||||
|
.collect(Collectors.joining(".", "", "." + userType.getSimpleName())) :
|
||||||
|
userType.getSimpleName());
|
||||||
|
Function<Method, String> methodFormatter =
|
||||||
|
method -> Arrays.stream(method.getParameterTypes())
|
||||||
|
.map(Class::getSimpleName)
|
||||||
|
.collect(Collectors.joining(",", "(", ")"));
|
||||||
|
return methods.entrySet().stream()
|
||||||
|
.map(e -> {
|
||||||
|
Method method = e.getKey();
|
||||||
|
return e.getValue() + ": " + method.getName() + methodFormatter.apply(method);
|
||||||
|
})
|
||||||
|
.collect(Collectors.joining("\n\t", "\n\t" + formattedType + ":" + "\n\t", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove handler methods and mapping based on plugin id.
|
||||||
|
*
|
||||||
|
* @param pluginId plugin id
|
||||||
|
*/
|
||||||
|
public void unregister(String pluginId) {
|
||||||
|
Assert.notNull(pluginId, "The pluginId must not be null.");
|
||||||
|
if (!pluginMappingInfo.containsKey(pluginId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pluginMappingInfo.remove(pluginId).forEach(this::unregisterMapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<RequestMappingInfo> getMappings(String pluginId) {
|
||||||
|
List<RequestMappingInfo> requestMappingInfos = pluginMappingInfo.get(pluginId);
|
||||||
|
if (requestMappingInfos == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return List.copyOf(requestMappingInfos);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected RequestMappingInfo getPluginMappingForMethod(String pluginId,
|
||||||
|
Method method, Class<?> handlerType) {
|
||||||
|
RequestMappingInfo info = super.getMappingForMethod(method, handlerType);
|
||||||
|
if (info != null) {
|
||||||
|
ApiVersion apiVersion = handlerType.getAnnotation(ApiVersion.class);
|
||||||
|
if (apiVersion == null) {
|
||||||
|
throw new PluginRuntimeException(
|
||||||
|
"The handler [" + handlerType + "] is missing @ApiVersion annotation.");
|
||||||
|
}
|
||||||
|
info = RequestMappingInfo.paths(buildPrefix(pluginId, apiVersion.value())).build()
|
||||||
|
.combine(info);
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String buildPrefix(String pluginId, String version) {
|
||||||
|
GroupVersion groupVersion = GroupVersion.parseAPIVersion(version);
|
||||||
|
return String.format("/api/%s/plugins/%s", groupVersion.version(), pluginId);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,10 @@
|
||||||
package run.halo.app.plugin;
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.pf4j.PluginWrapper;
|
import org.pf4j.PluginWrapper;
|
||||||
import org.springframework.context.support.GenericApplicationContext;
|
import org.springframework.context.support.GenericApplicationContext;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.util.ReflectionUtils;
|
|
||||||
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
|
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -19,56 +16,25 @@ import org.springframework.web.reactive.result.method.annotation.RequestMappingH
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class PluginRequestMappingManager {
|
public class PluginRequestMappingManager {
|
||||||
|
|
||||||
private final RequestMappingHandlerMapping requestMappingHandlerMapping;
|
private final PluginRequestMappingHandlerMapping requestMappingHandlerMapping;
|
||||||
|
|
||||||
public PluginRequestMappingManager(
|
public PluginRequestMappingManager(
|
||||||
RequestMappingHandlerMapping requestMappingHandlerMapping) {
|
PluginRequestMappingHandlerMapping pluginRequestMappingHandlerMapping) {
|
||||||
this.requestMappingHandlerMapping = requestMappingHandlerMapping;
|
this.requestMappingHandlerMapping = pluginRequestMappingHandlerMapping;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void registerControllers(PluginWrapper pluginWrapper) {
|
public void registerHandlerMappings(PluginWrapper pluginWrapper) {
|
||||||
String pluginId = pluginWrapper.getPluginId();
|
String pluginId = pluginWrapper.getPluginId();
|
||||||
getControllerBeans(pluginId)
|
getControllerBeans(pluginId)
|
||||||
.forEach(this::registerController);
|
.forEach(handler ->
|
||||||
|
requestMappingHandlerMapping.registerHandlerMethods(pluginId, handler));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void registerController(Object controller) {
|
public void removeHandlerMappings(String pluginId) {
|
||||||
log.debug("Registering plugin request mapping for bean: [{}]", controller);
|
requestMappingHandlerMapping.unregister(pluginId);
|
||||||
Method detectHandlerMethods = ReflectionUtils.findMethod(RequestMappingHandlerMapping.class,
|
|
||||||
"detectHandlerMethods", Object.class);
|
|
||||||
if (detectHandlerMethods == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
detectHandlerMethods.setAccessible(true);
|
|
||||||
detectHandlerMethods.invoke(requestMappingHandlerMapping, controller);
|
|
||||||
} catch (IllegalStateException ise) {
|
|
||||||
// ignore this
|
|
||||||
log.warn(ise.getMessage());
|
|
||||||
} catch (IllegalAccessException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} catch (InvocationTargetException e) {
|
|
||||||
log.warn("invocation target exception: [{}]", e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void unregisterControllerMappingInternal(Object controller) {
|
private Collection<Object> getControllerBeans(String pluginId) {
|
||||||
requestMappingHandlerMapping.getHandlerMethods()
|
|
||||||
.forEach((mapping, handlerMethod) -> {
|
|
||||||
if (controller == handlerMethod.getBean()) {
|
|
||||||
log.debug("Removed plugin request mapping [{}] from bean [{}]", mapping,
|
|
||||||
controller);
|
|
||||||
requestMappingHandlerMapping.unregisterMapping(mapping);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeControllerMapping(String pluginId) {
|
|
||||||
getControllerBeans(pluginId)
|
|
||||||
.forEach(this::unregisterControllerMappingInternal);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Collection<Object> getControllerBeans(String pluginId) {
|
|
||||||
GenericApplicationContext pluginContext =
|
GenericApplicationContext pluginContext =
|
||||||
ExtensionContextRegistry.getInstance().getByPluginId(pluginId);
|
ExtensionContextRegistry.getInstance().getByPluginId(pluginId);
|
||||||
return pluginContext.getBeansWithAnnotation(Controller.class).values();
|
return pluginContext.getBeansWithAnnotation(Controller.class).values();
|
||||||
|
|
|
@ -65,4 +65,6 @@ public interface Attributes {
|
||||||
* @return returns the path of the request
|
* @return returns the path of the request
|
||||||
*/
|
*/
|
||||||
String getPath();
|
String getPath();
|
||||||
|
|
||||||
|
String pluginName();
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,4 +67,9 @@ public class AttributesRecord implements Attributes {
|
||||||
public String getPath() {
|
public String getPath() {
|
||||||
return requestInfo.getPath();
|
return requestInfo.getPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String pluginName() {
|
||||||
|
return requestInfo.getPluginName();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import java.util.Objects;
|
||||||
import org.apache.commons.lang3.ArrayUtils;
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import run.halo.app.core.extension.Role;
|
import run.halo.app.core.extension.Role;
|
||||||
|
import run.halo.app.core.extension.Role.PolicyRule;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author guqing
|
* @author guqing
|
||||||
|
@ -38,7 +39,8 @@ public class RbacRequestEvaluation {
|
||||||
return verbMatches(rule, requestAttributes.getVerb())
|
return verbMatches(rule, requestAttributes.getVerb())
|
||||||
&& apiGroupMatches(rule, requestAttributes.getApiGroup())
|
&& apiGroupMatches(rule, requestAttributes.getApiGroup())
|
||||||
&& resourceMatches(rule, combinedResource, requestAttributes.getSubresource())
|
&& resourceMatches(rule, combinedResource, requestAttributes.getSubresource())
|
||||||
&& resourceNameMatches(rule, requestAttributes.getName());
|
&& resourceNameMatches(rule, requestAttributes.getName())
|
||||||
|
&& pluginNameMatches(rule, requestAttributes.pluginName());
|
||||||
}
|
}
|
||||||
return verbMatches(rule, requestAttributes.getVerb())
|
return verbMatches(rule, requestAttributes.getVerb())
|
||||||
&& nonResourceURLMatches(rule, requestAttributes.getPath());
|
&& nonResourceURLMatches(rule, requestAttributes.getPath());
|
||||||
|
@ -123,4 +125,8 @@ public class RbacRequestEvaluation {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected boolean pluginNameMatches(PolicyRule rule, String pluginName) {
|
||||||
|
return StringUtils.equals(rule.getPluginName(), pluginName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,11 @@ import java.util.Objects;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* RequestInfo holds information parsed from the {@link ServerHttpRequest}.
|
||||||
|
*
|
||||||
* @author guqing
|
* @author guqing
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
|
@ -14,7 +17,7 @@ import org.apache.commons.lang3.StringUtils;
|
||||||
public class RequestInfo {
|
public class RequestInfo {
|
||||||
boolean isResourceRequest;
|
boolean isResourceRequest;
|
||||||
final String path;
|
final String path;
|
||||||
String namespace;
|
String pluginName;
|
||||||
String verb;
|
String verb;
|
||||||
String apiPrefix;
|
String apiPrefix;
|
||||||
String apiGroup;
|
String apiGroup;
|
||||||
|
@ -28,14 +31,14 @@ public class RequestInfo {
|
||||||
this(isResourceRequest, path, null, verb, null, null, null, null, null, null, null);
|
this(isResourceRequest, path, null, verb, null, null, null, null, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public RequestInfo(boolean isResourceRequest, String path, String namespace, String verb,
|
public RequestInfo(boolean isResourceRequest, String path, String pluginName, String verb,
|
||||||
String apiPrefix,
|
String apiPrefix,
|
||||||
String apiGroup,
|
String apiGroup,
|
||||||
String apiVersion, String resource, String subresource, String name,
|
String apiVersion, String resource, String subresource, String name,
|
||||||
String[] parts) {
|
String[] parts) {
|
||||||
this.isResourceRequest = isResourceRequest;
|
this.isResourceRequest = isResourceRequest;
|
||||||
this.path = StringUtils.defaultString(path, "");
|
this.path = StringUtils.defaultString(path, "");
|
||||||
this.namespace = StringUtils.defaultString(namespace, "");
|
this.pluginName = StringUtils.defaultString(pluginName, "");
|
||||||
this.verb = StringUtils.defaultString(verb, "");
|
this.verb = StringUtils.defaultString(verb, "");
|
||||||
this.apiPrefix = StringUtils.defaultString(apiPrefix, "");
|
this.apiPrefix = StringUtils.defaultString(apiPrefix, "");
|
||||||
this.apiGroup = StringUtils.defaultString(apiGroup, "");
|
this.apiGroup = StringUtils.defaultString(apiGroup, "");
|
||||||
|
|
|
@ -8,6 +8,8 @@ import org.springframework.http.server.PathContainer;
|
||||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Creates {@link RequestInfo} from {@link ServerHttpRequest}.
|
||||||
|
*
|
||||||
* @author guqing
|
* @author guqing
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
|
@ -25,6 +27,9 @@ public class RequestInfoFactory {
|
||||||
*/
|
*/
|
||||||
final Set<String> grouplessApiPrefixes;
|
final Set<String> grouplessApiPrefixes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* special verbs no subresources.
|
||||||
|
*/
|
||||||
final Set<String> specialVerbs;
|
final Set<String> specialVerbs;
|
||||||
|
|
||||||
public RequestInfoFactory(Set<String> apiPrefixes, Set<String> grouplessApiPrefixes) {
|
public RequestInfoFactory(Set<String> apiPrefixes, Set<String> grouplessApiPrefixes) {
|
||||||
|
@ -39,39 +44,35 @@ public class RequestInfoFactory {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* newRequestInfo returns the information from the http request. If error is not occurred,
|
* <p>newRequestInfo returns the information from the http request. If error is not occurred,
|
||||||
* RequestInfo holds the information as best it is known before the failure
|
* RequestInfo holds the information as best it is known before the failure
|
||||||
* It handles both resource and non-resource requests and fills in all the pertinent
|
* It handles both resource and non-resource requests and fills in all the pertinent
|
||||||
* information
|
* information.</p>
|
||||||
* for each.
|
* <p>for each.</p>
|
||||||
* <p>
|
|
||||||
* Valid Inputs:
|
* Valid Inputs:
|
||||||
* <p>
|
* <p>Resource paths</p>
|
||||||
* Resource paths
|
|
||||||
* <pre>
|
* <pre>
|
||||||
* /apis/{api-group}/{version}/namespaces
|
* /api/{version}/plugins
|
||||||
* /api/{version}/namespaces
|
* /api/{version}/plugins/{pluginName}
|
||||||
* /api/{version}/namespaces/{namespace}
|
* /api/{version}/plugins/{pluginName}/{resource}
|
||||||
* /api/{version}/namespaces/{namespace}/{resource}
|
* /api/{version}/plugins/{pluginName}/{resource}/{resourceName}
|
||||||
* /api/{version}/namespaces/{namespace}/{resource}/{resourceName}
|
|
||||||
* /api/{version}/{resource}
|
* /api/{version}/{resource}
|
||||||
* /api/{version}/{resource}/{resourceName}
|
* /api/{version}/{resource}/{resourceName}
|
||||||
* </pre>
|
* </pre>
|
||||||
*
|
* <p>Special verbs without subresources:</p>
|
||||||
* <pre>
|
* <pre>
|
||||||
* Special verbs without subresources:
|
|
||||||
* /api/{version}/proxy/{resource}/{resourceName}
|
* /api/{version}/proxy/{resource}/{resourceName}
|
||||||
* /api/{version}/proxy/namespaces/{namespace}/{resource}/{resourceName}
|
* /api/{version}/proxy/namespaces/{namespace}/{resource}/{resourceName}
|
||||||
* </pre>
|
* </pre>
|
||||||
*
|
*
|
||||||
|
* <p>Special verbs with subresources:</p>
|
||||||
* <pre>
|
* <pre>
|
||||||
* Special verbs with subresources:
|
|
||||||
* /api/{version}/watch/{resource}
|
* /api/{version}/watch/{resource}
|
||||||
* /api/{version}/watch/namespaces/{namespace}/{resource}
|
* /api/{version}/watch/namespaces/{namespace}/{resource}
|
||||||
* </pre>
|
* </pre>
|
||||||
*
|
*
|
||||||
|
* <p>NonResource paths:</p>
|
||||||
* <pre>
|
* <pre>
|
||||||
* NonResource paths
|
|
||||||
* /apis/{api-group}/{version}
|
* /apis/{api-group}/{version}
|
||||||
* /apis/{api-group}
|
* /apis/{api-group}
|
||||||
* /apis
|
* /apis
|
||||||
|
@ -137,29 +138,29 @@ public class RequestInfoFactory {
|
||||||
default -> "";
|
default -> "";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Set<String> namespaceSubresources = Set.of("status", "finalize");
|
// URL forms: /plugins/{plugin-name}/{kind}/*, where parts are adjusted to be relative
|
||||||
// URL forms: /namespaces/{namespace}/{kind}/*, where parts are adjusted to be relative
|
|
||||||
// to kind
|
// to kind
|
||||||
if (Objects.equals(currentParts[0], "namespaces")) {
|
if (Objects.equals(currentParts[0], "plugins")
|
||||||
|
&& StringUtils.isEmpty(requestInfo.getApiGroup())) {
|
||||||
if (currentParts.length > 1) {
|
if (currentParts.length > 1) {
|
||||||
requestInfo.namespace = currentParts[1];
|
requestInfo.pluginName = currentParts[1];
|
||||||
|
|
||||||
// if there is another step after the namespace name and it is not a known
|
// if there is another step after the plugin name and it is not a known
|
||||||
// namespace subresource
|
// plugins subresource
|
||||||
// move currentParts to include it as a resource in its own right
|
// move currentParts to include it as a resource in its own right
|
||||||
if (currentParts.length > 2 && !namespaceSubresources.contains(currentParts[2])) {
|
if (currentParts.length > 2) {
|
||||||
currentParts = Arrays.copyOfRange(currentParts, 2, currentParts.length);
|
currentParts = Arrays.copyOfRange(currentParts, 2, currentParts.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
requestInfo.namespace = "";
|
requestInfo.pluginName = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// parsing successful, so we now know the proper value for .Parts
|
// parsing successful, so we now know the proper value for .Parts
|
||||||
requestInfo.parts = currentParts;
|
requestInfo.parts = currentParts;
|
||||||
Set<String> specialVerbsNoSubresources = Set.of("proxy");
|
// special verbs no subresources
|
||||||
// parts look like: resource/resourceName/subresource/other/stuff/we/don't/interpret
|
// parts look like: resource/resourceName/subresource/other/stuff/we/don't/interpret
|
||||||
if (requestInfo.parts.length >= 3 && !specialVerbsNoSubresources.contains(
|
if (requestInfo.parts.length >= 3 && !specialVerbs.contains(
|
||||||
requestInfo.verb)) {
|
requestInfo.verb)) {
|
||||||
requestInfo.subresource = requestInfo.parts[2];
|
requestInfo.subresource = requestInfo.parts[2];
|
||||||
}
|
}
|
||||||
|
@ -195,7 +196,7 @@ public class RequestInfoFactory {
|
||||||
return "1".equals(requestParam) || "true".equals(requestParam);
|
return "1".equals(requestParam) || "true".equals(requestParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String[] splitPath(String path) {
|
private String[] splitPath(String path) {
|
||||||
path = StringUtils.strip(path, "/");
|
path = StringUtils.strip(path, "/");
|
||||||
if (StringUtils.isEmpty(path)) {
|
if (StringUtils.isEmpty(path)) {
|
||||||
return new String[] {};
|
return new String[] {};
|
||||||
|
|
|
@ -42,10 +42,11 @@ class ExtensionConfigurationTest {
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
// disable authorization
|
// disable authorization
|
||||||
var rule = new Role.PolicyRule();
|
var rule = new Role.PolicyRule.Builder()
|
||||||
rule.setApiGroups(new String[] {"*"});
|
.apiGroups("*")
|
||||||
rule.setResources(new String[] {"*"});
|
.resources("*")
|
||||||
rule.setVerbs(new String[] {"*"});
|
.verbs("*")
|
||||||
|
.build();
|
||||||
var role = new Role();
|
var role = new Role();
|
||||||
role.setRules(List.of(rule));
|
role.setRules(List.of(rule));
|
||||||
when(roleService.getRole(anyString())).thenReturn(role);
|
when(roleService.getRole(anyString())).thenReturn(role);
|
||||||
|
|
|
@ -0,0 +1,301 @@
|
||||||
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.springframework.mock.http.server.reactive.MockServerHttpRequest.get;
|
||||||
|
import static org.springframework.mock.http.server.reactive.MockServerHttpRequest.post;
|
||||||
|
import static org.springframework.web.bind.annotation.RequestMethod.GET;
|
||||||
|
import static org.springframework.web.bind.annotation.RequestMethod.HEAD;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.pf4j.PluginRuntimeException;
|
||||||
|
import org.springframework.core.MethodIntrospector;
|
||||||
|
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.context.support.StaticWebApplicationContext;
|
||||||
|
import org.springframework.web.method.HandlerMethod;
|
||||||
|
import org.springframework.web.reactive.result.method.RequestMappingInfo;
|
||||||
|
import org.springframework.web.server.MethodNotAllowedException;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import org.springframework.web.util.pattern.PathPatternParser;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link PluginRequestMappingHandlerMapping}.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
class PluginRequestMappingHandlerMappingTest {
|
||||||
|
|
||||||
|
private final StaticWebApplicationContext wac = new StaticWebApplicationContext();
|
||||||
|
|
||||||
|
private PluginRequestMappingHandlerMapping handlerMapping;
|
||||||
|
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setup() {
|
||||||
|
handlerMapping = new PluginRequestMappingHandlerMapping();
|
||||||
|
this.handlerMapping.setApplicationContext(wac);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldAddPathPrefixWhenExistingApiVersion() throws Exception {
|
||||||
|
Method method = UserController.class.getMethod("getUser");
|
||||||
|
RequestMappingInfo info =
|
||||||
|
this.handlerMapping.getPluginMappingForMethod("fakePlugin", method,
|
||||||
|
UserController.class);
|
||||||
|
|
||||||
|
assertThat(info).isNotNull();
|
||||||
|
assertThat(info.getPatternsCondition().getPatterns()).isEqualTo(
|
||||||
|
Collections.singleton(
|
||||||
|
new PathPatternParser().parse("/api/v1alpha1/plugins/fakePlugin/user/{id}")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldFailWhenMissingApiVersion() throws Exception {
|
||||||
|
Method method = AppleMissingApiVersionController.class.getMethod("getName");
|
||||||
|
assertThatThrownBy(() ->
|
||||||
|
this.handlerMapping.getPluginMappingForMethod("fakePlugin", method,
|
||||||
|
AppleMissingApiVersionController.class)).isInstanceOf(PluginRuntimeException.class)
|
||||||
|
.hasMessage(
|
||||||
|
"The handler [class run.halo.app.plugin"
|
||||||
|
+ ".PluginRequestMappingHandlerMappingTest$AppleMissingApiVersionController] "
|
||||||
|
+ "is missing @ApiVersion annotation.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void registerHandlerMethods() {
|
||||||
|
assertThat(handlerMapping.getMappings("fakePlugin")).isEmpty();
|
||||||
|
|
||||||
|
UserController userController = mock(UserController.class);
|
||||||
|
handlerMapping.registerHandlerMethods("fakePlugin", userController);
|
||||||
|
|
||||||
|
List<RequestMappingInfo> mappings = handlerMapping.getMappings("fakePlugin");
|
||||||
|
assertThat(mappings).hasSize(1);
|
||||||
|
assertThat(mappings.get(0).toString()).isEqualTo(
|
||||||
|
"{GET /api/v1alpha1/plugins/fakePlugin/user/{id}}");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unregister() {
|
||||||
|
UserController userController = mock(UserController.class);
|
||||||
|
// register handler methods first
|
||||||
|
handlerMapping.registerHandlerMethods("fakePlugin", userController);
|
||||||
|
assertThat(handlerMapping.getMappings("fakePlugin")).hasSize(1);
|
||||||
|
|
||||||
|
// unregister
|
||||||
|
handlerMapping.unregister("fakePlugin");
|
||||||
|
assertThat(handlerMapping.getMappings("fakePlugin")).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getHandlerDirectMatch() {
|
||||||
|
// register handler methods first
|
||||||
|
handlerMapping.registerHandlerMethods("fakePlugin", new TestController());
|
||||||
|
|
||||||
|
// resolve an expected method from TestController
|
||||||
|
Method expected =
|
||||||
|
ResolvableMethod.on(TestController.class).annot(getMapping("/foo")).build();
|
||||||
|
|
||||||
|
// get handler by mock exchange
|
||||||
|
ServerWebExchange exchange =
|
||||||
|
MockServerWebExchange.from(get("/api/v1alpha1/plugins/fakePlugin/foo"));
|
||||||
|
HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(exchange).block();
|
||||||
|
|
||||||
|
assertThat(hm).isNotNull();
|
||||||
|
assertThat(hm.getMethod()).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getHandlerBestMatch() {
|
||||||
|
// register handler methods first
|
||||||
|
handlerMapping.registerHandlerMethods("fakePlugin", new TestController());
|
||||||
|
|
||||||
|
Method expected =
|
||||||
|
ResolvableMethod.on(TestController.class).annot(getMapping("/foo").params("p")).build();
|
||||||
|
|
||||||
|
String requestPath = "/api/v1alpha1/plugins/fakePlugin/foo?p=anything";
|
||||||
|
ServerWebExchange exchange = MockServerWebExchange.from(get(requestPath));
|
||||||
|
HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(exchange).block();
|
||||||
|
|
||||||
|
assertThat(hm).isNotNull();
|
||||||
|
assertThat(hm.getMethod()).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getHandlerRootPathMatch() {
|
||||||
|
// register handler methods first
|
||||||
|
handlerMapping.registerHandlerMethods("fakePlugin", new TestController());
|
||||||
|
Method expected =
|
||||||
|
ResolvableMethod.on(TestController.class).annot(getMapping("")).build();
|
||||||
|
|
||||||
|
String requestPath = "/api/v1alpha1/plugins/fakePlugin";
|
||||||
|
ServerWebExchange exchange = MockServerWebExchange.from(get(requestPath));
|
||||||
|
HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(exchange).block();
|
||||||
|
|
||||||
|
assertThat(hm).isNotNull();
|
||||||
|
assertThat(hm.getMethod()).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getHandlerRequestMethodNotAllowed() {
|
||||||
|
// register handler methods first
|
||||||
|
handlerMapping.registerHandlerMethods("fakePlugin", new TestController());
|
||||||
|
|
||||||
|
String requestPath = "/api/v1alpha1/plugins/fakePlugin/bar";
|
||||||
|
ServerWebExchange exchange = MockServerWebExchange.from(post(requestPath));
|
||||||
|
Mono<Object> mono = this.handlerMapping.getHandler(exchange);
|
||||||
|
|
||||||
|
assertError(mono, MethodNotAllowedException.class,
|
||||||
|
ex -> assertThat(ex.getSupportedMethods()).isEqualTo(
|
||||||
|
Set.of(HttpMethod.GET, HttpMethod.HEAD)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private <T> void assertError(Mono<Object> mono, final Class<T> exceptionClass,
|
||||||
|
final Consumer<T> consumer) {
|
||||||
|
StepVerifier.create(mono)
|
||||||
|
.consumeErrorWith(error -> {
|
||||||
|
assertThat(error.getClass()).isEqualTo(exceptionClass);
|
||||||
|
consumer.accept((T) error);
|
||||||
|
})
|
||||||
|
.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
private RequestMappingPredicate getMapping(String... path) {
|
||||||
|
return new RequestMappingPredicate(path).method(GET).params();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ResolvableMethod {
|
||||||
|
private final Class<?> objectClass;
|
||||||
|
private final List<Predicate<Method>> filters = new ArrayList<>(4);
|
||||||
|
|
||||||
|
public ResolvableMethod(Class<?> objectClass) {
|
||||||
|
this.objectClass = objectClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ResolvableMethod on(Class<?> objectClass) {
|
||||||
|
return new ResolvableMethod(objectClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResolvableMethod annot(Predicate<Method> predicate) {
|
||||||
|
filters.add(predicate);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Method build() {
|
||||||
|
Set<Method> methods = MethodIntrospector.selectMethods(this.objectClass, this::isMatch);
|
||||||
|
Assert.state(!methods.isEmpty(), () -> "No matching method: " + this);
|
||||||
|
Assert.state(methods.size() == 1,
|
||||||
|
() -> "Multiple matching methods: " + this + formatMethods(methods));
|
||||||
|
return methods.iterator().next();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatMethods(Set<Method> methods) {
|
||||||
|
return "\nMatched:\n" + methods.stream()
|
||||||
|
.map(Method::toGenericString).collect(Collectors.joining(",\n\t", "[\n\t", "\n]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isMatch(Method method) {
|
||||||
|
return this.filters.stream().allMatch(p -> p.test(method));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class RequestMappingPredicate implements Predicate<Method> {
|
||||||
|
|
||||||
|
private final String[] path;
|
||||||
|
|
||||||
|
private RequestMethod[] method = {};
|
||||||
|
|
||||||
|
private String[] params;
|
||||||
|
|
||||||
|
|
||||||
|
private RequestMappingPredicate(String... path) {
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public RequestMappingPredicate method(RequestMethod... methods) {
|
||||||
|
this.method = methods;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RequestMappingPredicate params(String... params) {
|
||||||
|
this.params = params;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean test(Method method) {
|
||||||
|
RequestMapping annot =
|
||||||
|
AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
|
||||||
|
return annot != null
|
||||||
|
&& Arrays.equals(this.path, annot.path())
|
||||||
|
&& Arrays.equals(this.method, annot.method())
|
||||||
|
&& (this.params == null || Arrays.equals(this.params, annot.params()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiVersion("v1alpha1")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/user")
|
||||||
|
static class UserController {
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public Principal getUser() {
|
||||||
|
return mock(Principal.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/apples")
|
||||||
|
static class AppleMissingApiVersionController {
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public String getName() {
|
||||||
|
return mock(String.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiVersion("v1alpha1")
|
||||||
|
@Controller
|
||||||
|
@RequestMapping
|
||||||
|
static class TestController {
|
||||||
|
@GetMapping("/foo")
|
||||||
|
public void foo() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(path = "/foo", params = "p")
|
||||||
|
public void fooParam() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequestMapping(path = "/ba*", method = {GET, HEAD})
|
||||||
|
public void bar() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("")
|
||||||
|
public void empty() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,6 @@ import run.halo.app.core.extension.service.DefaultRoleBindingService;
|
||||||
* @author guqing
|
* @author guqing
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
// @ExtendWith(SpringExtension.class)
|
|
||||||
public class DefaultRoleBindingServiceTest {
|
public class DefaultRoleBindingServiceTest {
|
||||||
|
|
||||||
private DefaultRoleBindingService roleBindingLister;
|
private DefaultRoleBindingService roleBindingLister;
|
||||||
|
|
|
@ -5,8 +5,10 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.json.JSONException;
|
||||||
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.skyscreamer.jsonassert.JSONAssert;
|
||||||
import run.halo.app.core.extension.Role;
|
import run.halo.app.core.extension.Role;
|
||||||
import run.halo.app.infra.utils.JsonUtils;
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
|
||||||
|
@ -25,21 +27,38 @@ class PolicyRuleTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void constructPolicyRule() throws JsonProcessingException {
|
public void constructPolicyRule() throws JsonProcessingException, JSONException {
|
||||||
Role.PolicyRule policyRule = new Role.PolicyRule(null, null, null, null, null);
|
Role.PolicyRule policyRule = new Role.PolicyRule(null, null, null, null, null, null);
|
||||||
assertThat(policyRule).isNotNull();
|
assertThat(policyRule).isNotNull();
|
||||||
JsonNode policyRuleJson = objectMapper.valueToTree(policyRule);
|
JSONAssert.assertEquals("""
|
||||||
assertThat(policyRuleJson).isEqualTo(objectMapper.readTree("""
|
{
|
||||||
{"apiGroups":[],"resources":[],"resourceNames":[],"nonResourceURLs":[],"verbs":[]}
|
"pluginName": "",
|
||||||
"""));
|
"apiGroups": [],
|
||||||
|
"resources": [],
|
||||||
|
"resourceNames": [],
|
||||||
|
"nonResourceURLs": [],
|
||||||
|
"verbs": []
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
JsonUtils.objectToJson(policyRule),
|
||||||
|
true);
|
||||||
|
|
||||||
Role.PolicyRule policyByBuilder = new Role.PolicyRule.Builder().build();
|
Role.PolicyRule policyByBuilder = new Role.PolicyRule.Builder().build();
|
||||||
JsonNode policyByBuilderJson = objectMapper.valueToTree(policyByBuilder);
|
JSONAssert.assertEquals("""
|
||||||
assertThat(policyByBuilderJson).isEqualTo(objectMapper.readTree("""
|
{
|
||||||
{"apiGroups":[],"resources":[],"resourceNames":[],"nonResourceURLs":[],"verbs":[]}
|
"pluginName": "",
|
||||||
"""));
|
"apiGroups": [],
|
||||||
|
"resources": [],
|
||||||
|
"resourceNames": [],
|
||||||
|
"nonResourceURLs": [],
|
||||||
|
"verbs": []
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
JsonUtils.objectToJson(policyByBuilder),
|
||||||
|
true);
|
||||||
|
|
||||||
Role.PolicyRule policyNonNull = new Role.PolicyRule.Builder()
|
Role.PolicyRule policyNonNull = new Role.PolicyRule.Builder()
|
||||||
|
.pluginName("fakePluginName")
|
||||||
.apiGroups("group")
|
.apiGroups("group")
|
||||||
.resources("resource-1", "resource-2")
|
.resources("resource-1", "resource-2")
|
||||||
.resourceNames("resourceName")
|
.resourceNames("resourceName")
|
||||||
|
@ -49,6 +68,7 @@ class PolicyRuleTest {
|
||||||
|
|
||||||
JsonNode expected = objectMapper.readTree("""
|
JsonNode expected = objectMapper.readTree("""
|
||||||
{
|
{
|
||||||
|
"pluginName": "fakePluginName",
|
||||||
"apiGroups": [
|
"apiGroups": [
|
||||||
"group"
|
"group"
|
||||||
],
|
],
|
||||||
|
|
|
@ -85,6 +85,67 @@ public class RequestInfoResolverTest {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void pluginsScopedAndPluginManage() {
|
||||||
|
List<SuccessCase> testCases =
|
||||||
|
List.of(new SuccessCase("DELETE", "/apis/extensions/v1/plugins/other/posts",
|
||||||
|
"delete", "apis", "extensions", "v1", "", "plugins", "posts", "other",
|
||||||
|
new String[] {"plugins", "other", "posts"}),
|
||||||
|
|
||||||
|
// api group identification
|
||||||
|
new SuccessCase("POST", "/apis/extensions/v1/plugins/other/posts", "create", "apis",
|
||||||
|
"extensions", "v1", "", "plugins", "posts", "other",
|
||||||
|
new String[] {"plugins", "other", "posts"}),
|
||||||
|
|
||||||
|
// api version identification
|
||||||
|
new SuccessCase("POST", "/apis/extensions/v1beta3/plugins/other/posts", "create",
|
||||||
|
"apis", "extensions", "v1beta3", "", "plugins", "posts", "other",
|
||||||
|
new String[] {"plugins", "other", "posts"}));
|
||||||
|
|
||||||
|
// 以 /apis 开头的 plugins 资源为 core 中管理插件使用的资源
|
||||||
|
for (SuccessCase successCase : testCases) {
|
||||||
|
var request =
|
||||||
|
method(HttpMethod.valueOf(successCase.method),
|
||||||
|
successCase.url).build();
|
||||||
|
RequestInfo requestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);
|
||||||
|
assertThat(requestInfo).isNotNull();
|
||||||
|
assertRequestInfoCase(successCase, requestInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SuccessCase> pluginScopedCases =
|
||||||
|
List.of(new SuccessCase("DELETE", "/api/v1/plugins/other/posts",
|
||||||
|
"deletecollection", "api", "", "v1", "other", "posts", "", "",
|
||||||
|
new String[] {"posts"}),
|
||||||
|
|
||||||
|
// api group identification
|
||||||
|
new SuccessCase("POST", "/api/v1/plugins/other/posts", "create", "api",
|
||||||
|
"", "v1", "other", "posts", "", "", new String[] {"posts"}),
|
||||||
|
|
||||||
|
// api version identification
|
||||||
|
new SuccessCase("POST", "/api/v1beta3/plugins/other/posts", "create",
|
||||||
|
"api", "", "v1beta3", "other", "posts", "", "",
|
||||||
|
new String[] {"posts"}));
|
||||||
|
|
||||||
|
for (SuccessCase pluginScopedCase : pluginScopedCases) {
|
||||||
|
var request =
|
||||||
|
method(HttpMethod.valueOf(pluginScopedCase.method),
|
||||||
|
pluginScopedCase.url).build();
|
||||||
|
RequestInfo requestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);
|
||||||
|
assertThat(requestInfo).isNotNull();
|
||||||
|
assertRequestInfoCase(pluginScopedCase, requestInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertRequestInfoCase(SuccessCase pluginScopedCase, RequestInfo requestInfo) {
|
||||||
|
assertThat(requestInfo.getPluginName()).isEqualTo(pluginScopedCase.expectedPluginName);
|
||||||
|
assertThat(requestInfo.getVerb()).isEqualTo(pluginScopedCase.expectedVerb);
|
||||||
|
assertThat(requestInfo.getParts()).isEqualTo(pluginScopedCase.expectedParts);
|
||||||
|
assertThat(requestInfo.getApiGroup()).isEqualTo(pluginScopedCase.expectedAPIGroup);
|
||||||
|
assertThat(requestInfo.getResource()).isEqualTo(pluginScopedCase.expectedResource);
|
||||||
|
assertThat(requestInfo.getSubresource())
|
||||||
|
.isEqualTo(pluginScopedCase.expectedSubresource());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void errorCaseTest() {
|
public void errorCaseTest() {
|
||||||
List<ErrorCases> errorCases = List.of(new ErrorCases("no resource path", "/"),
|
List<ErrorCases> errorCases = List.of(new ErrorCases("no resource path", "/"),
|
||||||
|
@ -113,6 +174,10 @@ public class RequestInfoResolverTest {
|
||||||
List<PolicyRule> rules = List.of(
|
List<PolicyRule> rules = List.of(
|
||||||
new PolicyRule.Builder().apiGroups("").resources("posts").verbs("list", "get")
|
new PolicyRule.Builder().apiGroups("").resources("posts").verbs("list", "get")
|
||||||
.build(),
|
.build(),
|
||||||
|
new PolicyRule.Builder().pluginName("fakePlugin").apiGroups("").resources("posts")
|
||||||
|
.verbs("list", "get").build(),
|
||||||
|
new PolicyRule.Builder().pluginName("fakePlugin").apiGroups("")
|
||||||
|
.resources("posts/tags").verbs("list", "get").build(),
|
||||||
new PolicyRule.Builder().apiGroups("").resources("categories").verbs("*").build(),
|
new PolicyRule.Builder().apiGroups("").resources("categories").verbs("*").build(),
|
||||||
new PolicyRule.Builder().nonResourceURLs("/healthy").verbs("get", "post", "head")
|
new PolicyRule.Builder().nonResourceURLs("/healthy").verbs("get", "post", "head")
|
||||||
.build());
|
.build());
|
||||||
|
@ -165,6 +230,11 @@ public class RequestInfoResolverTest {
|
||||||
new RequestResolveCase("/api/v1/posts", "DELETE", false),
|
new RequestResolveCase("/api/v1/posts", "DELETE", false),
|
||||||
new RequestResolveCase("/api/v1/posts/aName", "UPDATE", false),
|
new RequestResolveCase("/api/v1/posts/aName", "UPDATE", false),
|
||||||
|
|
||||||
|
// plugin resource
|
||||||
|
new RequestResolveCase("/api/v1/plugins/fakePlugin/posts", "GET", true),
|
||||||
|
new RequestResolveCase("/api/v1/plugins/fakePlugin/posts/some-name", "GET", true),
|
||||||
|
new RequestResolveCase("/api/v1/plugins/fakePlugin/posts/some-name/tags", "GET", true),
|
||||||
|
|
||||||
// group resource url
|
// group resource url
|
||||||
new RequestResolveCase("/apis/group/v1/posts", "GET", false),
|
new RequestResolveCase("/apis/group/v1/posts", "GET", false),
|
||||||
|
|
||||||
|
@ -185,7 +255,7 @@ public class RequestInfoResolverTest {
|
||||||
|
|
||||||
public record SuccessCase(String method, String url, String expectedVerb,
|
public record SuccessCase(String method, String url, String expectedVerb,
|
||||||
String expectedAPIPrefix, String expectedAPIGroup,
|
String expectedAPIPrefix, String expectedAPIGroup,
|
||||||
String expectedAPIVersion, String expectedNamespace,
|
String expectedAPIVersion, String expectedPluginName,
|
||||||
String expectedResource, String expectedSubresource,
|
String expectedResource, String expectedSubresource,
|
||||||
String expectedName, String[] expectedParts) {
|
String expectedName, String[] expectedParts) {
|
||||||
}
|
}
|
||||||
|
@ -194,31 +264,31 @@ public class RequestInfoResolverTest {
|
||||||
List<SuccessCase> getTestRequestInfos() {
|
List<SuccessCase> getTestRequestInfos() {
|
||||||
String namespaceAll = "*";
|
String namespaceAll = "*";
|
||||||
return List.of(
|
return List.of(
|
||||||
new SuccessCase("GET", "/api/v1/namespaces", "list", "api", "", "v1", "", "namespaces",
|
new SuccessCase("GET", "/api/v1/plugins", "list", "api", "", "v1", "", "plugins",
|
||||||
"", "", new String[] {"namespaces"}),
|
"", "", new String[] {"plugins"}),
|
||||||
new SuccessCase("GET", "/api/v1/namespaces/other", "get", "api", "", "v1", "other",
|
new SuccessCase("GET", "/api/v1/plugins/other", "get", "api", "", "v1", "other",
|
||||||
"namespaces", "", "other", new String[] {"namespaces", "other"}),
|
"plugins", "", "other", new String[] {"plugins", "other"}),
|
||||||
|
|
||||||
new SuccessCase("GET", "/api/v1/namespaces/other/posts", "list", "api", "", "v1",
|
new SuccessCase("GET", "/api/v1/plugins/other/posts", "list", "api", "", "v1",
|
||||||
"other", "posts", "", "", new String[] {"posts"}),
|
"other", "posts", "", "", new String[] {"posts"}),
|
||||||
new SuccessCase("GET", "/api/v1/namespaces/other/posts/foo", "get", "api", "", "v1",
|
new SuccessCase("GET", "/api/v1/plugins/other/posts/foo", "get", "api", "", "v1",
|
||||||
"other", "posts", "", "foo", new String[] {"posts", "foo"}),
|
"other", "posts", "", "foo", new String[] {"posts", "foo"}),
|
||||||
new SuccessCase("HEAD", "/api/v1/namespaces/other/posts/foo", "get", "api", "", "v1",
|
new SuccessCase("HEAD", "/api/v1/plugins/other/posts/foo", "get", "api", "", "v1",
|
||||||
"other", "posts", "", "foo", new String[] {"posts", "foo"}),
|
"other", "posts", "", "foo", new String[] {"posts", "foo"}),
|
||||||
new SuccessCase("GET", "/api/v1/posts", "list", "api", "", "v1", namespaceAll, "posts",
|
new SuccessCase("GET", "/api/v1/posts", "list", "api", "", "v1", namespaceAll, "posts",
|
||||||
"", "", new String[] {"posts"}),
|
"", "", new String[] {"posts"}),
|
||||||
new SuccessCase("HEAD", "/api/v1/posts", "list", "api", "", "v1", namespaceAll, "posts",
|
new SuccessCase("HEAD", "/api/v1/posts", "list", "api", "", "v1", namespaceAll, "posts",
|
||||||
"", "", new String[] {"posts"}),
|
"", "", new String[] {"posts"}),
|
||||||
new SuccessCase("GET", "/api/v1/namespaces/other/posts/foo", "get", "api", "", "v1",
|
new SuccessCase("GET", "/api/v1/plugins/other/posts/foo", "get", "api", "", "v1",
|
||||||
"other", "posts", "", "foo", new String[] {"posts", "foo"}),
|
"other", "posts", "", "foo", new String[] {"posts", "foo"}),
|
||||||
new SuccessCase("GET", "/api/v1/namespaces/other/posts", "list", "api", "", "v1",
|
new SuccessCase("GET", "/api/v1/plugins/other/posts", "list", "api", "", "v1",
|
||||||
"other", "posts", "", "", new String[] {"posts"}),
|
"other", "posts", "", "", new String[] {"posts"}),
|
||||||
|
|
||||||
// special verbs
|
// special verbs
|
||||||
new SuccessCase("GET", "/api/v1/proxy/namespaces/other/posts/foo", "proxy", "api", "",
|
new SuccessCase("GET", "/api/v1/proxy/plugins/other/posts/foo", "proxy", "api", "",
|
||||||
"v1", "other", "posts", "", "foo", new String[] {"posts", "foo"}),
|
"v1", "other", "posts", "", "foo", new String[] {"posts", "foo"}),
|
||||||
new SuccessCase("GET",
|
new SuccessCase("GET",
|
||||||
"/api/v1/proxy/namespaces/other/posts/foo/subpath/not/a/subresource", "proxy",
|
"/api/v1/proxy/plugins/other/posts/foo/subpath/not/a/subresource", "proxy",
|
||||||
"api", "", "v1", "other", "posts", "", "foo",
|
"api", "", "v1", "other", "posts", "", "foo",
|
||||||
new String[] {"posts", "foo", "subpath", "not", "a", "subresource"}),
|
new String[] {"posts", "foo", "subpath", "not", "a", "subresource"}),
|
||||||
new SuccessCase("GET", "/api/v1/watch/posts", "watch", "api", "", "v1", namespaceAll,
|
new SuccessCase("GET", "/api/v1/watch/posts", "watch", "api", "", "v1", namespaceAll,
|
||||||
|
@ -227,52 +297,35 @@ public class RequestInfoResolverTest {
|
||||||
namespaceAll, "posts", "", "", new String[] {"posts"}),
|
namespaceAll, "posts", "", "", new String[] {"posts"}),
|
||||||
new SuccessCase("GET", "/api/v1/posts?watch=false", "list", "api", "", "v1",
|
new SuccessCase("GET", "/api/v1/posts?watch=false", "list", "api", "", "v1",
|
||||||
namespaceAll, "posts", "", "", new String[] {"posts"}),
|
namespaceAll, "posts", "", "", new String[] {"posts"}),
|
||||||
new SuccessCase("GET", "/api/v1/watch/namespaces/other/posts", "watch", "api", "", "v1",
|
new SuccessCase("GET", "/api/v1/watch/plugins/other/posts", "watch", "api", "", "v1",
|
||||||
"other", "posts", "", "", new String[] {"posts"}),
|
"other", "posts", "", "", new String[] {"posts"}),
|
||||||
new SuccessCase("GET", "/api/v1/namespaces/other/posts?watch=1", "watch", "api", "",
|
new SuccessCase("GET", "/api/v1/plugins/other/posts?watch=1", "watch", "api", "",
|
||||||
"v1", "other", "posts", "", "", new String[] {"posts"}),
|
"v1", "other", "posts", "", "", new String[] {"posts"}),
|
||||||
new SuccessCase("GET", "/api/v1/namespaces/other/posts?watch=0", "list", "api", "",
|
new SuccessCase("GET", "/api/v1/plugins/other/posts?watch=0", "list", "api", "",
|
||||||
"v1", "other", "posts", "", "", new String[] {"posts"}),
|
"v1", "other", "posts", "", "", new String[] {"posts"}),
|
||||||
|
|
||||||
// subresource identification
|
// subresource identification
|
||||||
new SuccessCase("GET", "/api/v1/namespaces/other/posts/foo/status", "get", "api", "",
|
new SuccessCase("GET", "/api/v1/plugins/other/posts/foo/status", "get", "api", "",
|
||||||
"v1", "other", "posts", "status", "foo", new String[] {"posts", "foo", "status"}),
|
"v1", "other", "posts", "status", "foo", new String[] {"posts", "foo", "status"}),
|
||||||
new SuccessCase("GET", "/api/v1/namespaces/other/posts/foo/proxy/subpath", "get", "api",
|
new SuccessCase("GET", "/api/v1/plugins/other/posts/foo/proxy/subpath", "get", "api",
|
||||||
"", "v1", "other", "posts", "proxy", "foo",
|
"", "v1", "other", "posts", "proxy", "foo",
|
||||||
new String[] {"posts", "foo", "proxy", "subpath"}),
|
new String[] {"posts", "foo", "proxy", "subpath"}),
|
||||||
new SuccessCase("PUT", "/api/v1/namespaces/other/finalize", "update", "api", "", "v1",
|
|
||||||
"other", "namespaces", "finalize", "other",
|
|
||||||
new String[] {"namespaces", "other", "finalize"}),
|
|
||||||
new SuccessCase("PUT", "/api/v1/namespaces/other/status", "update", "api", "", "v1",
|
|
||||||
"other", "namespaces", "status", "other",
|
|
||||||
new String[] {"namespaces", "other", "status"}),
|
|
||||||
|
|
||||||
// verb identification
|
// verb identification
|
||||||
new SuccessCase("PATCH", "/api/v1/namespaces/other/posts/foo", "patch", "api", "", "v1",
|
new SuccessCase("PATCH", "/api/v1/plugins/other/posts/foo", "patch", "api", "", "v1",
|
||||||
"other", "posts", "", "foo", new String[] {"posts", "foo"}),
|
"other", "posts", "", "foo", new String[] {"posts", "foo"}),
|
||||||
new SuccessCase("DELETE", "/api/v1/namespaces/other/posts/foo", "delete", "api", "",
|
new SuccessCase("DELETE", "/api/v1/plugins/other/posts/foo", "delete", "api", "",
|
||||||
"v1", "other", "posts", "", "foo", new String[] {"posts", "foo"}),
|
"v1", "other", "posts", "", "foo", new String[] {"posts", "foo"}),
|
||||||
new SuccessCase("POST", "/api/v1/namespaces/other/posts", "create", "api", "", "v1",
|
new SuccessCase("POST", "/api/v1/plugins/other/posts", "create", "api", "", "v1",
|
||||||
"other", "posts", "", "", new String[] {"posts"}),
|
"other", "posts", "", "", new String[] {"posts"}),
|
||||||
|
|
||||||
// deletecollection verb identification
|
// deletecollection verb identification
|
||||||
new SuccessCase("DELETE", "/api/v1/nodes", "deletecollection", "api", "", "v1", "",
|
new SuccessCase("DELETE", "/api/v1/nodes", "deletecollection", "api", "", "v1", "",
|
||||||
"nodes", "", "", new String[] {"nodes"}),
|
"nodes", "", "", new String[] {"nodes"}),
|
||||||
new SuccessCase("DELETE", "/api/v1/namespaces", "deletecollection", "api", "", "v1", "",
|
new SuccessCase("DELETE", "/api/v1/plugins", "deletecollection", "api", "", "v1", "",
|
||||||
"namespaces", "", "", new String[] {"namespaces"}),
|
"plugins", "", "", new String[] {"plugins"}),
|
||||||
new SuccessCase("DELETE", "/api/v1/namespaces/other/posts", "deletecollection", "api",
|
new SuccessCase("DELETE", "/api/v1/plugins/other/posts", "deletecollection", "api",
|
||||||
"", "v1", "other", "posts", "", "", new String[] {"posts"}),
|
"", "v1", "other", "posts", "", "", new String[] {"posts"}));
|
||||||
new SuccessCase("DELETE", "/apis/extensions/v1/namespaces/other/posts",
|
|
||||||
"deletecollection", "apis", "extensions", "v1", "other", "posts", "", "",
|
|
||||||
new String[] {"posts"}),
|
|
||||||
|
|
||||||
// api group identification
|
|
||||||
new SuccessCase("POST", "/apis/extensions/v1/namespaces/other/posts", "create", "apis",
|
|
||||||
"extensions", "v1", "other", "posts", "", "", new String[] {"posts"}),
|
|
||||||
|
|
||||||
// api version identification
|
|
||||||
new SuccessCase("POST", "/apis/extensions/v1beta3/namespaces/other/posts", "create",
|
|
||||||
"apis", "extensions", "v1beta3", "other", "posts", "", "", new String[] {"posts"}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue