Make RestController pluggable (#98187)

This commit changes the ActionModules to allow  the RestController to be
provided by an internal plugin.

It renames  `RestInterceptorActionPlugin` to `RestServerActionPlugin`
and adds a new `getRestController` method to it.

There may be multiple RestServerActionPlugins installed on a node, but
only 1 may provide a Rest Wrapper (getRestHandlerInterceptor) and only 1
may provide a RestController (getRestController).
This commit is contained in:
Tim Vernum 2023-08-08 15:29:24 +10:00 committed by GitHub
parent 2aca985e29
commit 3093c40b8b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 341 additions and 42 deletions

View file

@ -0,0 +1,92 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package co.elastic.elasticsearch.test;
import org.elasticsearch.client.internal.node.NodeClient;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.indices.breaker.CircuitBreakerService;
import org.elasticsearch.logging.LogManager;
import org.elasticsearch.logging.Logger;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.plugins.interceptor.RestServerActionPlugin;
import org.elasticsearch.rest.RestChannel;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestHandler;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.tracing.Tracer;
import org.elasticsearch.usage.UsageService;
import java.util.function.UnaryOperator;
public class CustomRestPlugin extends Plugin implements RestServerActionPlugin {
private static final Logger logger = LogManager.getLogger(CustomRestPlugin.class);
private static void echoHeader(String name, RestRequest request, ThreadContext threadContext) {
var value = request.header(name);
if (value != null) {
threadContext.addResponseHeader(name, value);
}
}
public static class CustomInterceptor implements RestHandler {
private final ThreadContext threadContext;
private final RestHandler delegate;
public CustomInterceptor(ThreadContext threadContext, RestHandler delegate) {
this.threadContext = threadContext;
this.delegate = delegate;
}
@Override
public void handleRequest(RestRequest request, RestChannel channel, NodeClient client) throws Exception {
logger.info("intercept request {} {}", request.method(), request.uri());
echoHeader("x-test-interceptor", request, threadContext);
delegate.handleRequest(request, channel, client);
}
}
public static class CustomController extends RestController {
public CustomController(
UnaryOperator<RestHandler> handlerWrapper,
NodeClient client,
CircuitBreakerService circuitBreakerService,
UsageService usageService,
Tracer tracer
) {
super(handlerWrapper, client, circuitBreakerService, usageService, tracer);
}
@Override
public void dispatchRequest(RestRequest request, RestChannel channel, ThreadContext threadContext) {
logger.info("dispatch request {} {}", request.method(), request.uri());
echoHeader("x-test-controller", request, threadContext);
super.dispatchRequest(request, channel, threadContext);
}
}
@Override
public UnaryOperator<RestHandler> getRestHandlerInterceptor(ThreadContext threadContext) {
return handler -> new CustomInterceptor(threadContext, handler);
}
@Override
public RestController getRestController(
UnaryOperator<RestHandler> handlerWrapper,
NodeClient client,
CircuitBreakerService circuitBreakerService,
UsageService usageService,
Tracer tracer
) {
return new CustomController(handlerWrapper, client, circuitBreakerService, usageService, tracer);
}
}

View file

@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.plugins.interceptor;
import co.elastic.elasticsearch.test.CustomRestPlugin;
import org.elasticsearch.client.Request;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.Response;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.test.ESIntegTestCase;
import java.io.IOException;
import java.util.Collection;
import static org.elasticsearch.common.util.CollectionUtils.appendToCopyNoNullElements;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.notNullValue;
public class CustomRestPluginIT extends ESIntegTestCase {
@Override
protected Collection<Class<? extends Plugin>> nodePlugins() {
return appendToCopyNoNullElements(super.nodePlugins(), CustomRestPlugin.class);
}
@Override
protected boolean addMockHttpTransport() {
return false; // enable http
}
public void testInterceptor() throws Exception {
var headerValue = randomAlphaOfLengthBetween(4, 12);
assertThat(doRequest("x-test-interceptor", headerValue), equalTo(headerValue));
assertThat(doRequest("x-test-interceptor", null), equalTo(null));
}
public void testController() throws Exception {
var headerValue = randomAlphaOfLengthBetween(4, 12);
assertThat(doRequest("x-test-controller", headerValue), equalTo(headerValue));
assertThat(doRequest("x-test-controller", null), equalTo(null));
}
private String doRequest(String headerName, String headerValue) throws IOException {
assertThat(headerName, notNullValue());
var client = getRestClient();
RequestOptions.Builder options = RequestOptions.DEFAULT.toBuilder();
if (headerValue != null) {
options.addHeader(headerName, headerValue);
}
var request = new Request("GET", "/_nodes/_local/plugins");
request.setOptions(options);
final Response response = client.performRequest(request);
return response.getHeader(headerName);
}
}