mirror of
https://github.com/elastic/elasticsearch.git
synced 2025-04-17 20:05:09 -04:00
Improve handling of empty response (#125562)
Today `ActionResponse$Empty` implements `ToXContentObject`, but yields no bytes of content when serialized which creates an invalid JSON response. This commit removes the bogus interface and adjusts the affected REST APIs to send a `text/plain` response instead.
This commit is contained in:
parent
a152b4e29b
commit
527d2a203b
22 changed files with 217 additions and 51 deletions
6
docs/changelog/125562.yaml
Normal file
6
docs/changelog/125562.yaml
Normal file
|
@ -0,0 +1,6 @@
|
|||
pr: 125562
|
||||
summary: Improve handling of empty response
|
||||
area: Infra/REST API
|
||||
type: bug
|
||||
issues:
|
||||
- 57639
|
|
@ -180,6 +180,11 @@ setup:
|
|||
- requires:
|
||||
cluster_features: ["gte_v8.8.0"]
|
||||
reason: "reset API added in in 8.8.0"
|
||||
test_runner_features: [ capabilities ]
|
||||
capabilities:
|
||||
- method: DELETE
|
||||
path: /_internal/desired_balance
|
||||
capabilities: [ plain_text_empty_response ]
|
||||
|
||||
- do:
|
||||
_internal.delete_desired_balance: { }
|
||||
|
|
|
@ -3,6 +3,11 @@ setup:
|
|||
- requires:
|
||||
cluster_features: ["gte_v8.13.0"]
|
||||
reason: "API added in in 8.1.0 but modified in 8.13 (node_version field removed)"
|
||||
test_runner_features: [ capabilities ]
|
||||
capabilities:
|
||||
- method: DELETE
|
||||
path: /_internal/desired_nodes
|
||||
capabilities: [ plain_text_empty_response ]
|
||||
---
|
||||
teardown:
|
||||
- do:
|
||||
|
|
|
@ -6,6 +6,11 @@ setup:
|
|||
- requires:
|
||||
cluster_features: ["gte_v8.3.0"]
|
||||
reason: "API added in in 8.1.0 but modified in 8.3"
|
||||
test_runner_features: [ capabilities ]
|
||||
capabilities:
|
||||
- method: DELETE
|
||||
path: /_internal/desired_nodes
|
||||
capabilities: [ plain_text_empty_response ]
|
||||
---
|
||||
teardown:
|
||||
- do:
|
||||
|
|
|
@ -3,6 +3,11 @@ setup:
|
|||
- requires:
|
||||
cluster_features: ["gte_v8.4.0"]
|
||||
reason: "Support for the dry run option was added in in 8.4.0"
|
||||
test_runner_features: [ capabilities ]
|
||||
capabilities:
|
||||
- method: DELETE
|
||||
path: /_internal/desired_nodes
|
||||
capabilities: [ plain_text_empty_response ]
|
||||
---
|
||||
teardown:
|
||||
- do:
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
setup:
|
||||
- requires:
|
||||
test_runner_features: [ capabilities ]
|
||||
capabilities:
|
||||
- method: POST
|
||||
path: /_cluster/voting_config_exclusions
|
||||
capabilities: [ plain_text_empty_response ]
|
||||
- method: DELETE
|
||||
path: /_cluster/voting_config_exclusions
|
||||
capabilities: [ plain_text_empty_response ]
|
||||
reason: needs these capabilities
|
||||
|
||||
---
|
||||
teardown:
|
||||
- do:
|
||||
cluster.delete_voting_config_exclusions: {}
|
||||
|
|
|
@ -9,22 +9,24 @@
|
|||
package org.elasticsearch.cluster.coordination;
|
||||
|
||||
import org.elasticsearch.ElasticsearchException;
|
||||
import org.elasticsearch.action.admin.cluster.configuration.AddVotingConfigExclusionsRequest;
|
||||
import org.elasticsearch.action.admin.cluster.configuration.TransportAddVotingConfigExclusionsAction;
|
||||
import org.elasticsearch.client.Request;
|
||||
import org.elasticsearch.client.Response;
|
||||
import org.elasticsearch.cluster.ClusterState;
|
||||
import org.elasticsearch.cluster.node.DiscoveryNode;
|
||||
import org.elasticsearch.cluster.service.ClusterService;
|
||||
import org.elasticsearch.common.Priority;
|
||||
import org.elasticsearch.plugins.Plugin;
|
||||
import org.elasticsearch.test.ESIntegTestCase;
|
||||
import org.elasticsearch.test.transport.MockTransportService;
|
||||
import org.elasticsearch.transport.TransportService;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import static org.hamcrest.Matchers.empty;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
|
@ -38,18 +40,39 @@ public class VotingConfigurationIT extends ESIntegTestCase {
|
|||
return Collections.singletonList(MockTransportService.TestPlugin.class);
|
||||
}
|
||||
|
||||
public void testAbdicateAfterVotingConfigExclusionAdded() throws ExecutionException, InterruptedException {
|
||||
@Override
|
||||
protected boolean addMockHttpTransport() {
|
||||
return false; // enable HTTP
|
||||
}
|
||||
|
||||
public void testAbdicateAfterVotingConfigExclusionAdded() throws IOException {
|
||||
internalCluster().setBootstrapMasterNodeIndex(0);
|
||||
internalCluster().startNodes(2);
|
||||
final String originalMaster = internalCluster().getMasterName();
|
||||
final var restClient = getRestClient();
|
||||
|
||||
logger.info("--> excluding master node {}", originalMaster);
|
||||
client().execute(
|
||||
TransportAddVotingConfigExclusionsAction.TYPE,
|
||||
new AddVotingConfigExclusionsRequest(TEST_REQUEST_TIMEOUT, originalMaster)
|
||||
).get();
|
||||
final var excludeRequest = new Request("POST", "/_cluster/voting_config_exclusions");
|
||||
excludeRequest.addParameter("node_names", originalMaster);
|
||||
assertEmptyResponse(restClient.performRequest(excludeRequest));
|
||||
|
||||
clusterAdmin().prepareHealth(TEST_REQUEST_TIMEOUT).setWaitForEvents(Priority.LANGUID).get();
|
||||
assertNotEquals(originalMaster, internalCluster().getMasterName());
|
||||
|
||||
final var clearRequest = new Request("DELETE", "/_cluster/voting_config_exclusions");
|
||||
clearRequest.addParameter("wait_for_removal", "false");
|
||||
assertEmptyResponse(restClient.performRequest(clearRequest));
|
||||
|
||||
assertThat(
|
||||
internalCluster().getInstance(ClusterService.class).state().metadata().coordinationMetadata().getVotingConfigExclusions(),
|
||||
empty()
|
||||
);
|
||||
}
|
||||
|
||||
private void assertEmptyResponse(Response response) throws IOException {
|
||||
assertEquals("text/plain; charset=UTF-8", response.getHeader("content-type"));
|
||||
assertEquals(0, response.getEntity().getContentLength());
|
||||
assertEquals(0, response.getEntity().getContent().readAllBytes().length);
|
||||
}
|
||||
|
||||
public void testElectsNodeNotInVotingConfiguration() throws Exception {
|
||||
|
|
|
@ -10,9 +10,10 @@
|
|||
package org.elasticsearch.action;
|
||||
|
||||
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||
import org.elasticsearch.rest.action.EmptyResponseListener;
|
||||
import org.elasticsearch.transport.TransportResponse;
|
||||
import org.elasticsearch.xcontent.ToXContentObject;
|
||||
import org.elasticsearch.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.xcontent.ToXContent;
|
||||
import org.elasticsearch.xcontent.XContent;
|
||||
|
||||
/**
|
||||
* Base class for responses to action requests.
|
||||
|
@ -21,20 +22,23 @@ public abstract class ActionResponse extends TransportResponse {
|
|||
|
||||
public ActionResponse() {}
|
||||
|
||||
public static final class Empty extends ActionResponse implements ToXContentObject {
|
||||
/**
|
||||
* A response with no payload. This is deliberately not an implementation of {@link ToXContent} or similar because an empty response
|
||||
* has no valid {@link XContent} representation. Use {@link EmptyResponseListener} to convert this to a valid (plain-text) REST
|
||||
* response instead.
|
||||
*/
|
||||
public static final class Empty extends ActionResponse {
|
||||
|
||||
private Empty() { /* singleton */ }
|
||||
|
||||
public static final ActionResponse.Empty INSTANCE = new ActionResponse.Empty();
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "EmptyActionResponse{}";
|
||||
return "ActionResponse.Empty{}";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(StreamOutput out) {}
|
||||
|
||||
@Override
|
||||
public XContentBuilder toXContent(final XContentBuilder builder, final Params params) {
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
package org.elasticsearch.rest.action;
|
||||
|
||||
import org.elasticsearch.action.ActionResponse;
|
||||
import org.elasticsearch.common.bytes.BytesArray;
|
||||
import org.elasticsearch.rest.RestChannel;
|
||||
import org.elasticsearch.rest.RestResponse;
|
||||
import org.elasticsearch.rest.RestStatus;
|
||||
|
||||
/**
|
||||
* A listener which converts a successful {@link ActionResponse.Empty} action response into a {@code 200 OK} REST response with empty body.
|
||||
*/
|
||||
public final class EmptyResponseListener extends RestResponseListener<ActionResponse.Empty> {
|
||||
public EmptyResponseListener(RestChannel channel) {
|
||||
super(channel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestResponse buildResponse(ActionResponse.Empty ignored) throws Exception {
|
||||
// Content-type header is not required for an empty body but some clients may expect it; the empty body is a valid text/plain entity
|
||||
// so we use that here.
|
||||
return new RestResponse(RestStatus.OK, RestResponse.TEXT_CONTENT_TYPE, BytesArray.EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Capability name for APIs that previously would return an invalid zero-byte {@code application/json} response so that the YAML test
|
||||
* runner can avoid those APIs.
|
||||
*/
|
||||
public static final String PLAIN_TEXT_EMPTY_RESPONSE_CAPABILITY_NAME = "plain_text_empty_response";
|
||||
}
|
|
@ -27,6 +27,7 @@ public abstract class RestBuilderListener<Response> extends RestResponseListener
|
|||
try (XContentBuilder builder = channel.newBuilder()) {
|
||||
final RestResponse restResponse = buildResponse(response, builder);
|
||||
assert assertBuilderClosed(builder);
|
||||
assert restResponse.content() != null && restResponse.content().length() > 0 : "Use EmptyResponseListener for empty responses";
|
||||
return restResponse;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,13 +16,15 @@ import org.elasticsearch.common.Strings;
|
|||
import org.elasticsearch.core.TimeValue;
|
||||
import org.elasticsearch.rest.BaseRestHandler;
|
||||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.rest.action.RestToXContentListener;
|
||||
import org.elasticsearch.rest.action.EmptyResponseListener;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.elasticsearch.rest.RestRequest.Method.POST;
|
||||
import static org.elasticsearch.rest.RestUtils.getMasterNodeTimeout;
|
||||
import static org.elasticsearch.rest.action.EmptyResponseListener.PLAIN_TEXT_EMPTY_RESPONSE_CAPABILITY_NAME;
|
||||
|
||||
public class RestAddVotingConfigExclusionAction extends BaseRestHandler {
|
||||
private static final TimeValue DEFAULT_TIMEOUT = TimeValue.timeValueSeconds(30L);
|
||||
|
@ -37,6 +39,11 @@ public class RestAddVotingConfigExclusionAction extends BaseRestHandler {
|
|||
return List.of(new Route(POST, "/_cluster/voting_config_exclusions"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> supportedCapabilities() {
|
||||
return Set.of(PLAIN_TEXT_EMPTY_RESPONSE_CAPABILITY_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canTripCircuitBreaker() {
|
||||
return false;
|
||||
|
@ -48,7 +55,7 @@ public class RestAddVotingConfigExclusionAction extends BaseRestHandler {
|
|||
return channel -> client.execute(
|
||||
TransportAddVotingConfigExclusionsAction.TYPE,
|
||||
votingConfigExclusionsRequest,
|
||||
new RestToXContentListener<>(channel)
|
||||
new EmptyResponseListener(channel)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -14,13 +14,15 @@ import org.elasticsearch.action.admin.cluster.configuration.TransportClearVoting
|
|||
import org.elasticsearch.client.internal.node.NodeClient;
|
||||
import org.elasticsearch.rest.BaseRestHandler;
|
||||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.rest.action.RestToXContentListener;
|
||||
import org.elasticsearch.rest.action.EmptyResponseListener;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.elasticsearch.rest.RestRequest.Method.DELETE;
|
||||
import static org.elasticsearch.rest.RestUtils.getMasterNodeTimeout;
|
||||
import static org.elasticsearch.rest.action.EmptyResponseListener.PLAIN_TEXT_EMPTY_RESPONSE_CAPABILITY_NAME;
|
||||
|
||||
public class RestClearVotingConfigExclusionsAction extends BaseRestHandler {
|
||||
|
||||
|
@ -39,10 +41,15 @@ public class RestClearVotingConfigExclusionsAction extends BaseRestHandler {
|
|||
return "clear_voting_config_exclusions_action";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> supportedCapabilities() {
|
||||
return Set.of(PLAIN_TEXT_EMPTY_RESPONSE_CAPABILITY_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException {
|
||||
final var req = resolveVotingConfigExclusionsRequest(request);
|
||||
return channel -> client.execute(TransportClearVotingConfigExclusionsAction.TYPE, req, new RestToXContentListener<>(channel));
|
||||
return channel -> client.execute(TransportClearVotingConfigExclusionsAction.TYPE, req, new EmptyResponseListener(channel));
|
||||
}
|
||||
|
||||
static ClearVotingConfigExclusionsRequest resolveVotingConfigExclusionsRequest(final RestRequest request) {
|
||||
|
|
|
@ -17,10 +17,13 @@ import org.elasticsearch.rest.RestRequest;
|
|||
import org.elasticsearch.rest.RestUtils;
|
||||
import org.elasticsearch.rest.Scope;
|
||||
import org.elasticsearch.rest.ServerlessScope;
|
||||
import org.elasticsearch.rest.action.RestToXContentListener;
|
||||
import org.elasticsearch.rest.action.EmptyResponseListener;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.elasticsearch.rest.action.EmptyResponseListener.PLAIN_TEXT_EMPTY_RESPONSE_CAPABILITY_NAME;
|
||||
|
||||
@ServerlessScope(Scope.INTERNAL)
|
||||
public class RestDeleteDesiredBalanceAction extends BaseRestHandler {
|
||||
|
@ -35,9 +38,14 @@ public class RestDeleteDesiredBalanceAction extends BaseRestHandler {
|
|||
return List.of(new Route(RestRequest.Method.DELETE, "_internal/desired_balance"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> supportedCapabilities() {
|
||||
return Set.of(PLAIN_TEXT_EMPTY_RESPONSE_CAPABILITY_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
|
||||
final var req = new DesiredBalanceRequest(RestUtils.getMasterNodeTimeout(request));
|
||||
return channel -> client.execute(TransportDeleteDesiredBalanceAction.TYPE, req, new RestToXContentListener<>(channel));
|
||||
return channel -> client.execute(TransportDeleteDesiredBalanceAction.TYPE, req, new EmptyResponseListener(channel));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,13 +14,15 @@ import org.elasticsearch.action.support.master.AcknowledgedRequest;
|
|||
import org.elasticsearch.client.internal.node.NodeClient;
|
||||
import org.elasticsearch.rest.BaseRestHandler;
|
||||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.rest.action.RestToXContentListener;
|
||||
import org.elasticsearch.rest.action.EmptyResponseListener;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.elasticsearch.rest.RestUtils.getAckTimeout;
|
||||
import static org.elasticsearch.rest.RestUtils.getMasterNodeTimeout;
|
||||
import static org.elasticsearch.rest.action.EmptyResponseListener.PLAIN_TEXT_EMPTY_RESPONSE_CAPABILITY_NAME;
|
||||
|
||||
public class RestDeleteDesiredNodesAction extends BaseRestHandler {
|
||||
@Override
|
||||
|
@ -33,13 +35,18 @@ public class RestDeleteDesiredNodesAction extends BaseRestHandler {
|
|||
return List.of(new Route(RestRequest.Method.DELETE, "_internal/desired_nodes"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> supportedCapabilities() {
|
||||
return Set.of(PLAIN_TEXT_EMPTY_RESPONSE_CAPABILITY_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
|
||||
final var deleteDesiredNodesRequest = new AcknowledgedRequest.Plain(getMasterNodeTimeout(request), getAckTimeout(request));
|
||||
return restChannel -> client.execute(
|
||||
TransportDeleteDesiredNodesAction.TYPE,
|
||||
deleteDesiredNodesRequest,
|
||||
new RestToXContentListener<>(restChannel)
|
||||
new EmptyResponseListener(restChannel)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,16 +23,19 @@ import java.util.concurrent.atomic.AtomicReference;
|
|||
|
||||
public class RestBuilderListenerTests extends ESTestCase {
|
||||
|
||||
// bypass the check that XContent responses are never empty - we're ignoring the builder and sending a text/plain response anyway
|
||||
private static final BytesArray NONEMPTY_BODY = new BytesArray(new byte[] { '\n' });
|
||||
|
||||
public void testXContentBuilderClosedInBuildResponse() throws Exception {
|
||||
AtomicReference<XContentBuilder> builderAtomicReference = new AtomicReference<>();
|
||||
RestBuilderListener<TransportResponse.Empty> builderListener = new RestBuilderListener<Empty>(
|
||||
RestBuilderListener<TransportResponse.Empty> builderListener = new RestBuilderListener<>(
|
||||
new FakeRestChannel(new FakeRestRequest(), randomBoolean(), 1)
|
||||
) {
|
||||
@Override
|
||||
public RestResponse buildResponse(Empty empty, XContentBuilder builder) throws Exception {
|
||||
public RestResponse buildResponse(Empty empty, XContentBuilder builder) {
|
||||
builderAtomicReference.set(builder);
|
||||
builder.close();
|
||||
return new RestResponse(RestStatus.OK, RestResponse.TEXT_CONTENT_TYPE, BytesArray.EMPTY);
|
||||
return new RestResponse(RestStatus.OK, RestResponse.TEXT_CONTENT_TYPE, NONEMPTY_BODY);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -43,13 +46,13 @@ public class RestBuilderListenerTests extends ESTestCase {
|
|||
|
||||
public void testXContentBuilderNotClosedInBuildResponseAssertionsDisabled() throws Exception {
|
||||
AtomicReference<XContentBuilder> builderAtomicReference = new AtomicReference<>();
|
||||
RestBuilderListener<TransportResponse.Empty> builderListener = new RestBuilderListener<Empty>(
|
||||
RestBuilderListener<TransportResponse.Empty> builderListener = new RestBuilderListener<>(
|
||||
new FakeRestChannel(new FakeRestRequest(), randomBoolean(), 1)
|
||||
) {
|
||||
@Override
|
||||
public RestResponse buildResponse(Empty empty, XContentBuilder builder) throws Exception {
|
||||
public RestResponse buildResponse(Empty empty, XContentBuilder builder) {
|
||||
builderAtomicReference.set(builder);
|
||||
return new RestResponse(RestStatus.OK, RestResponse.TEXT_CONTENT_TYPE, BytesArray.EMPTY);
|
||||
return new RestResponse(RestStatus.OK, RestResponse.TEXT_CONTENT_TYPE, NONEMPTY_BODY);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -64,15 +67,15 @@ public class RestBuilderListenerTests extends ESTestCase {
|
|||
assertTrue(builderAtomicReference.get().generator().isClosed());
|
||||
}
|
||||
|
||||
public void testXContentBuilderNotClosedInBuildResponseAssertionsEnabled() throws Exception {
|
||||
public void testXContentBuilderNotClosedInBuildResponseAssertionsEnabled() {
|
||||
assumeTrue("tests are not being run with assertions", RestBuilderListener.class.desiredAssertionStatus());
|
||||
|
||||
RestBuilderListener<TransportResponse.Empty> builderListener = new RestBuilderListener<Empty>(
|
||||
RestBuilderListener<TransportResponse.Empty> builderListener = new RestBuilderListener<>(
|
||||
new FakeRestChannel(new FakeRestRequest(), randomBoolean(), 1)
|
||||
) {
|
||||
@Override
|
||||
public RestResponse buildResponse(Empty empty, XContentBuilder builder) throws Exception {
|
||||
return new RestResponse(RestStatus.OK, RestResponse.TEXT_CONTENT_TYPE, BytesArray.EMPTY);
|
||||
public RestResponse buildResponse(Empty empty, XContentBuilder builder) {
|
||||
return new RestResponse(RestStatus.OK, RestResponse.TEXT_CONTENT_TYPE, NONEMPTY_BODY);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -64,6 +64,6 @@ public class QueryVectorBuilderTests extends AbstractQueryVectorBuilderTestCase<
|
|||
|
||||
@Override
|
||||
protected ActionResponse createResponse(float[] array, TestQueryVectorBuilderPlugin.TestQueryVectorBuilder builder) {
|
||||
return new ActionResponse.Empty();
|
||||
return ActionResponse.Empty.INSTANCE;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,6 +50,9 @@ public class ClientYamlTestResponse {
|
|||
byte[] bytes = EntityUtils.toByteArray(response.getEntity());
|
||||
// skip parsing if we got text back (e.g. if we called _cat apis)
|
||||
if (bodyContentType != null) {
|
||||
if (bytes.length == 0) {
|
||||
throw new IllegalArgumentException("Empty body is invalid for content-type [" + contentType + "]");
|
||||
}
|
||||
this.parsedResponse = ObjectPath.createFromXContent(bodyContentType.xContent(), new BytesArray(bytes));
|
||||
}
|
||||
this.body = bytes;
|
||||
|
|
|
@ -67,7 +67,7 @@ public class DeprecationCacheResetAction extends ActionType<DeprecationCacheRese
|
|||
}
|
||||
}
|
||||
|
||||
public static class Response extends BaseNodesResponse<NodeResponse> implements Writeable, ToXContentObject {
|
||||
public static class Response extends BaseNodesResponse<NodeResponse> implements Writeable {
|
||||
public Response(ClusterName clusterName, List<NodeResponse> nodes, List<FailedNodeException> failures) {
|
||||
super(clusterName, nodes, failures);
|
||||
}
|
||||
|
@ -82,11 +82,6 @@ public class DeprecationCacheResetAction extends ActionType<DeprecationCacheRese
|
|||
TransportAction.localOnly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public XContentBuilder toXContent(XContentBuilder builder, Params params) {
|
||||
return builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
|
|
@ -7,15 +7,17 @@
|
|||
|
||||
package org.elasticsearch.xpack.deprecation.logging;
|
||||
|
||||
import org.elasticsearch.action.ActionResponse;
|
||||
import org.elasticsearch.client.internal.node.NodeClient;
|
||||
import org.elasticsearch.rest.BaseRestHandler;
|
||||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.rest.action.RestToXContentListener;
|
||||
import org.elasticsearch.rest.action.EmptyResponseListener;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.elasticsearch.rest.RestRequest.Method.DELETE;
|
||||
import static org.elasticsearch.rest.action.EmptyResponseListener.PLAIN_TEXT_EMPTY_RESPONSE_CAPABILITY_NAME;
|
||||
|
||||
public class RestDeprecationCacheResetAction extends BaseRestHandler {
|
||||
|
||||
|
@ -30,8 +32,17 @@ public class RestDeprecationCacheResetAction extends BaseRestHandler {
|
|||
}
|
||||
|
||||
@Override
|
||||
public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
|
||||
public Set<String> supportedCapabilities() {
|
||||
return Set.of(PLAIN_TEXT_EMPTY_RESPONSE_CAPABILITY_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) {
|
||||
DeprecationCacheResetAction.Request resetRequest = new DeprecationCacheResetAction.Request();
|
||||
return channel -> client.execute(DeprecationCacheResetAction.INSTANCE, resetRequest, new RestToXContentListener<>(channel));
|
||||
return channel -> client.execute(
|
||||
DeprecationCacheResetAction.INSTANCE,
|
||||
resetRequest,
|
||||
new EmptyResponseListener(channel).map(ignored -> ActionResponse.Empty.INSTANCE)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,15 +12,19 @@ import org.elasticsearch.common.bytes.BytesArray;
|
|||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.test.rest.FakeRestRequest;
|
||||
import org.elasticsearch.test.rest.RestActionTestCase;
|
||||
import org.elasticsearch.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.xcontent.XContentType;
|
||||
import org.elasticsearch.xpack.core.ml.action.CreateTrainedModelAssignmentAction;
|
||||
import org.elasticsearch.xpack.core.ml.action.UpdateTrainedModelDeploymentAction;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class RestUpdateTrainedModelDeploymentActionTests extends RestActionTestCase {
|
||||
public void testNumberOfAllocationInParam() {
|
||||
|
@ -33,7 +37,7 @@ public class RestUpdateTrainedModelDeploymentActionTests extends RestActionTestC
|
|||
assertEquals(request.getNumberOfAllocations().intValue(), 5);
|
||||
|
||||
executeCalled.set(true);
|
||||
return mock(CreateTrainedModelAssignmentAction.Response.class);
|
||||
return newMockResponse();
|
||||
}));
|
||||
var params = new HashMap<String, String>();
|
||||
params.put("number_of_allocations", "5");
|
||||
|
@ -56,7 +60,7 @@ public class RestUpdateTrainedModelDeploymentActionTests extends RestActionTestC
|
|||
assertEquals(request.getNumberOfAllocations().intValue(), 6);
|
||||
|
||||
executeCalled.set(true);
|
||||
return mock(CreateTrainedModelAssignmentAction.Response.class);
|
||||
return newMockResponse();
|
||||
}));
|
||||
|
||||
final String content = """
|
||||
|
@ -69,4 +73,16 @@ public class RestUpdateTrainedModelDeploymentActionTests extends RestActionTestC
|
|||
dispatchRequest(inferenceRequest);
|
||||
assertThat(executeCalled.get(), equalTo(true));
|
||||
}
|
||||
|
||||
private static CreateTrainedModelAssignmentAction.Response newMockResponse() {
|
||||
final var response = mock(CreateTrainedModelAssignmentAction.Response.class);
|
||||
try {
|
||||
when(response.toXContent(any(), any())).thenAnswer(
|
||||
invocation -> asInstanceOf(XContentBuilder.class, invocation.getArgument(0)).startObject().endObject()
|
||||
);
|
||||
} catch (IOException e) {
|
||||
fail(e);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
package org.elasticsearch.xpack.security.rest.action.profile;
|
||||
|
||||
import org.apache.lucene.search.TotalHits;
|
||||
import org.elasticsearch.ElasticsearchSecurityException;
|
||||
import org.elasticsearch.common.bytes.BytesArray;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
|
@ -33,7 +34,6 @@ import static org.hamcrest.Matchers.containsString;
|
|||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
@ -54,7 +54,7 @@ public class RestSuggestProfilesActionTests extends RestActionTestCase {
|
|||
verifyingClient.setExecuteLocallyVerifier(((actionType, actionRequest) -> {
|
||||
assertThat(actionRequest, instanceOf(SuggestProfilesRequest.class));
|
||||
requestHolder.set((SuggestProfilesRequest) actionRequest);
|
||||
return mock(SuggestProfilesResponse.class);
|
||||
return new SuggestProfilesResponse(new SuggestProfilesResponse.ProfileHit[0], 0, new TotalHits(0, TotalHits.Relation.EQUAL_TO));
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,13 @@
|
|||
"Test operator privileges will work in the mixed cluster":
|
||||
|
||||
- requires:
|
||||
test_runner_features: headers
|
||||
test_runner_features: [headers, capabilities]
|
||||
cluster_features: ["gte_v7.11.0"]
|
||||
reason: "operator privileges are available since 7.11"
|
||||
capabilities:
|
||||
- method: DELETE
|
||||
path: /_cluster/voting_config_exclusions
|
||||
capabilities: [ plain_text_empty_response ]
|
||||
|
||||
# The default user ("test_user") is an operator, so this works
|
||||
- do:
|
||||
|
|
Loading…
Add table
Reference in a new issue