From a1001e11e16de0923de45947ab1ab2d19b6c3cd6 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 25 Mar 2025 08:09:39 +0000 Subject: [PATCH 1/9] Improve handling of empty response 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. --- .../coordination/VotingConfigurationIT.java | 39 +++++++++++++++---- .../elasticsearch/action/ActionResponse.java | 22 ++++++----- .../rest/action/EmptyResponseListener.java | 32 +++++++++++++++ .../rest/action/RestBuilderListener.java | 1 + .../RestAddVotingConfigExclusionAction.java | 4 +- ...RestClearVotingConfigExclusionsAction.java | 4 +- .../RestDeleteDesiredBalanceAction.java | 4 +- .../cluster/RestDeleteDesiredNodesAction.java | 4 +- .../vectors/QueryVectorBuilderTests.java | 2 +- .../rest/yaml/ClientYamlTestResponse.java | 3 ++ 10 files changed, 89 insertions(+), 26 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/rest/action/EmptyResponseListener.java diff --git a/server/src/internalClusterTest/java/org/elasticsearch/cluster/coordination/VotingConfigurationIT.java b/server/src/internalClusterTest/java/org/elasticsearch/cluster/coordination/VotingConfigurationIT.java index fd03591ebb1c5..6cf3363d43e38 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/cluster/coordination/VotingConfigurationIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/cluster/coordination/VotingConfigurationIT.java @@ -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 @@ protected Collection> nodePlugins() { 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 { diff --git a/server/src/main/java/org/elasticsearch/action/ActionResponse.java b/server/src/main/java/org/elasticsearch/action/ActionResponse.java index 2b56e94729487..000756bc7465a 100644 --- a/server/src/main/java/org/elasticsearch/action/ActionResponse.java +++ b/server/src/main/java/org/elasticsearch/action/ActionResponse.java @@ -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; - } } } diff --git a/server/src/main/java/org/elasticsearch/rest/action/EmptyResponseListener.java b/server/src/main/java/org/elasticsearch/rest/action/EmptyResponseListener.java new file mode 100644 index 0000000000000..128046a369074 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/rest/action/EmptyResponseListener.java @@ -0,0 +1,32 @@ +/* + * 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 { + 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); + } +} diff --git a/server/src/main/java/org/elasticsearch/rest/action/RestBuilderListener.java b/server/src/main/java/org/elasticsearch/rest/action/RestBuilderListener.java index f3fcbc7ef9345..bf675ff3b1574 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/RestBuilderListener.java +++ b/server/src/main/java/org/elasticsearch/rest/action/RestBuilderListener.java @@ -27,6 +27,7 @@ public final RestResponse buildResponse(Response response) throws Exception { 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; } } diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestAddVotingConfigExclusionAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestAddVotingConfigExclusionAction.java index e66b7d2b0b1a4..82893237b22fd 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestAddVotingConfigExclusionAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestAddVotingConfigExclusionAction.java @@ -16,7 +16,7 @@ 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; @@ -48,7 +48,7 @@ protected RestChannelConsumer prepareRequest(final RestRequest request, final No return channel -> client.execute( TransportAddVotingConfigExclusionsAction.TYPE, votingConfigExclusionsRequest, - new RestToXContentListener<>(channel) + new EmptyResponseListener(channel) ); } diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClearVotingConfigExclusionsAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClearVotingConfigExclusionsAction.java index 1e1b3742bf454..06266b6e15673 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClearVotingConfigExclusionsAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClearVotingConfigExclusionsAction.java @@ -14,7 +14,7 @@ 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; @@ -42,7 +42,7 @@ public String getName() { @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) { diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestDeleteDesiredBalanceAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestDeleteDesiredBalanceAction.java index 16da7208279e8..aff57e62dea9c 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestDeleteDesiredBalanceAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestDeleteDesiredBalanceAction.java @@ -17,7 +17,7 @@ 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; @@ -38,6 +38,6 @@ public List routes() { @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)); } } diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestDeleteDesiredNodesAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestDeleteDesiredNodesAction.java index abf66504f1c48..0be3a728a7d7d 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestDeleteDesiredNodesAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestDeleteDesiredNodesAction.java @@ -14,7 +14,7 @@ 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; @@ -39,7 +39,7 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli return restChannel -> client.execute( TransportDeleteDesiredNodesAction.TYPE, deleteDesiredNodesRequest, - new RestToXContentListener<>(restChannel) + new EmptyResponseListener(restChannel) ); } } diff --git a/server/src/test/java/org/elasticsearch/search/vectors/QueryVectorBuilderTests.java b/server/src/test/java/org/elasticsearch/search/vectors/QueryVectorBuilderTests.java index a12f218db3450..29e73d598eb40 100644 --- a/server/src/test/java/org/elasticsearch/search/vectors/QueryVectorBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/search/vectors/QueryVectorBuilderTests.java @@ -64,6 +64,6 @@ protected void doAssertClientRequest(ActionRequest request, TestQueryVectorBuild @Override protected ActionResponse createResponse(float[] array, TestQueryVectorBuilderPlugin.TestQueryVectorBuilder builder) { - return new ActionResponse.Empty(); + return ActionResponse.Empty.INSTANCE; } } diff --git a/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/ClientYamlTestResponse.java b/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/ClientYamlTestResponse.java index 5dc45ebabfdb0..2ed9c66e24368 100644 --- a/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/ClientYamlTestResponse.java +++ b/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/ClientYamlTestResponse.java @@ -50,6 +50,9 @@ public ClientYamlTestResponse(Response response) throws IOException { 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; From df0496c4bacb7c209bc381d19b6a305bcaaaf6c1 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 25 Mar 2025 08:23:37 +0000 Subject: [PATCH 2/9] Update docs/changelog/125562.yaml --- docs/changelog/125562.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/changelog/125562.yaml diff --git a/docs/changelog/125562.yaml b/docs/changelog/125562.yaml new file mode 100644 index 0000000000000..66c6b81c14a8e --- /dev/null +++ b/docs/changelog/125562.yaml @@ -0,0 +1,5 @@ +pr: 125562 +summary: Improve handling of empty response +area: Infra/REST API +type: bug +issues: [] From 407d7a7483c97261968286e87676c086d23c91e0 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 25 Mar 2025 08:26:01 +0000 Subject: [PATCH 3/9] Update docs/changelog/125562.yaml --- docs/changelog/125562.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog/125562.yaml b/docs/changelog/125562.yaml index 66c6b81c14a8e..1e53662fabb7e 100644 --- a/docs/changelog/125562.yaml +++ b/docs/changelog/125562.yaml @@ -2,4 +2,5 @@ pr: 125562 summary: Improve handling of empty response area: Infra/REST API type: bug -issues: [] +issues: + - 57639 From 07c444a1c4d280c596df5cea8d0f0446395dda2e Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 25 Mar 2025 09:03:00 +0000 Subject: [PATCH 4/9] Mocks --- .../rest/action/RestBuilderListenerTests.java | 23 +++++++++++-------- ...dateTrainedModelDeploymentActionTests.java | 14 ++++++++++- .../RestSuggestProfilesActionTests.java | 4 ++-- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/rest/action/RestBuilderListenerTests.java b/server/src/test/java/org/elasticsearch/rest/action/RestBuilderListenerTests.java index 827a07b89b2b8..d278e027e857f 100644 --- a/server/src/test/java/org/elasticsearch/rest/action/RestBuilderListenerTests.java +++ b/server/src/test/java/org/elasticsearch/rest/action/RestBuilderListenerTests.java @@ -23,16 +23,19 @@ 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 builderAtomicReference = new AtomicReference<>(); - RestBuilderListener builderListener = new RestBuilderListener( + RestBuilderListener 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 RestResponse buildResponse(Empty empty, XContentBuilder builder) throws E public void testXContentBuilderNotClosedInBuildResponseAssertionsDisabled() throws Exception { AtomicReference builderAtomicReference = new AtomicReference<>(); - RestBuilderListener builderListener = new RestBuilderListener( + RestBuilderListener 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 @@ boolean assertBuilderClosed(XContentBuilder xContentBuilder) { 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 builderListener = new RestBuilderListener( + RestBuilderListener 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); } }; diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/rest/inference/RestUpdateTrainedModelDeploymentActionTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/rest/inference/RestUpdateTrainedModelDeploymentActionTests.java index cce6b284a524d..ac0bbccd4711f 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/rest/inference/RestUpdateTrainedModelDeploymentActionTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/rest/inference/RestUpdateTrainedModelDeploymentActionTests.java @@ -12,15 +12,19 @@ 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,15 @@ public void testNumberOfAllocationInParam() { assertEquals(request.getNumberOfAllocations().intValue(), 5); executeCalled.set(true); - return mock(CreateTrainedModelAssignmentAction.Response.class); + 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; })); var params = new HashMap(); params.put("number_of_allocations", "5"); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/profile/RestSuggestProfilesActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/profile/RestSuggestProfilesActionTests.java index 26412106229a1..8ee48ebc0f986 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/profile/RestSuggestProfilesActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/profile/RestSuggestProfilesActionTests.java @@ -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.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 void init() { 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)); })); } From f90bec4451a09bc91135e6d315bf90a52d7ebc16 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 25 Mar 2025 09:38:59 +0000 Subject: [PATCH 5/9] Fix mock --- ...dateTrainedModelDeploymentActionTests.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/rest/inference/RestUpdateTrainedModelDeploymentActionTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/rest/inference/RestUpdateTrainedModelDeploymentActionTests.java index ac0bbccd4711f..a05ceb3cc5adf 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/rest/inference/RestUpdateTrainedModelDeploymentActionTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/rest/inference/RestUpdateTrainedModelDeploymentActionTests.java @@ -37,15 +37,7 @@ public void testNumberOfAllocationInParam() { assertEquals(request.getNumberOfAllocations().intValue(), 5); executeCalled.set(true); - 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; + return newMockResponse(); })); var params = new HashMap(); params.put("number_of_allocations", "5"); @@ -68,7 +60,7 @@ public void testNumberOfAllocationInBody() { assertEquals(request.getNumberOfAllocations().intValue(), 6); executeCalled.set(true); - return mock(CreateTrainedModelAssignmentAction.Response.class); + return newMockResponse(); })); final String content = """ @@ -81,4 +73,16 @@ public void testNumberOfAllocationInBody() { 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; + } } From d0db9ace78a1e7de04d1cf5ee9af1d008b6265da Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 25 Mar 2025 11:16:54 +0000 Subject: [PATCH 6/9] Filter tests by capabilities --- .../test/cluster.desired_nodes/10_basic.yml | 5 +++++ .../cluster.desired_nodes/11_old_format.yml | 5 +++++ .../test/cluster.desired_nodes/20_dry_run.yml | 5 +++++ .../10_basic.yml | 13 +++++++++++++ .../rest/action/EmptyResponseListener.java | 6 ++++++ .../RestAddVotingConfigExclusionAction.java | 7 +++++++ ...RestClearVotingConfigExclusionsAction.java | 7 +++++++ .../RestDeleteDesiredBalanceAction.java | 8 ++++++++ .../cluster/RestDeleteDesiredNodesAction.java | 7 +++++++ .../logging/DeprecationCacheResetAction.java | 7 +------ .../RestDeprecationCacheResetAction.java | 19 +++++++++++++++---- 11 files changed, 79 insertions(+), 10 deletions(-) diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_nodes/10_basic.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_nodes/10_basic.yml index a45146a4e147a..97efac652ef7d 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_nodes/10_basic.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_nodes/10_basic.yml @@ -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_response ] --- teardown: - do: diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_nodes/11_old_format.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_nodes/11_old_format.yml index 4df1252bef273..637ae09d8508b 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_nodes/11_old_format.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_nodes/11_old_format.yml @@ -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_response ] --- teardown: - do: diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_nodes/20_dry_run.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_nodes/20_dry_run.yml index 87fc015347348..83889eab62c66 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_nodes/20_dry_run.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_nodes/20_dry_run.yml @@ -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_response ] --- teardown: - do: diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.voting_config_exclusions/10_basic.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.voting_config_exclusions/10_basic.yml index 327344c8daf55..0db1c60e832f2 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.voting_config_exclusions/10_basic.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.voting_config_exclusions/10_basic.yml @@ -1,3 +1,16 @@ +setup: + - requires: + test_runner_features: [ capabilities ] + capabilities: + - method: POST + path: /_cluster/voting_config_exclusions + capabilities: [ plain_text_response ] + - method: DELETE + path: /_cluster/voting_config_exclusions + capabilities: [ plain_text_response ] + reason: needs these capabilities + +--- teardown: - do: cluster.delete_voting_config_exclusions: {} diff --git a/server/src/main/java/org/elasticsearch/rest/action/EmptyResponseListener.java b/server/src/main/java/org/elasticsearch/rest/action/EmptyResponseListener.java index 128046a369074..29f81ad99612f 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/EmptyResponseListener.java +++ b/server/src/main/java/org/elasticsearch/rest/action/EmptyResponseListener.java @@ -29,4 +29,10 @@ public RestResponse buildResponse(ActionResponse.Empty ignored) throws Exception // 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_RESPONSE_CAPABILITY_NAME = "plain_text_response"; } diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestAddVotingConfigExclusionAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestAddVotingConfigExclusionAction.java index 82893237b22fd..62142c5c795de 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestAddVotingConfigExclusionAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestAddVotingConfigExclusionAction.java @@ -20,9 +20,11 @@ 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_RESPONSE_CAPABILITY_NAME; public class RestAddVotingConfigExclusionAction extends BaseRestHandler { private static final TimeValue DEFAULT_TIMEOUT = TimeValue.timeValueSeconds(30L); @@ -37,6 +39,11 @@ public List routes() { return List.of(new Route(POST, "/_cluster/voting_config_exclusions")); } + @Override + public Set supportedCapabilities() { + return Set.of(PLAIN_TEXT_RESPONSE_CAPABILITY_NAME); + } + @Override public boolean canTripCircuitBreaker() { return false; diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClearVotingConfigExclusionsAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClearVotingConfigExclusionsAction.java index 06266b6e15673..0703b886ed3b1 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClearVotingConfigExclusionsAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClearVotingConfigExclusionsAction.java @@ -18,9 +18,11 @@ 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_RESPONSE_CAPABILITY_NAME; public class RestClearVotingConfigExclusionsAction extends BaseRestHandler { @@ -39,6 +41,11 @@ public String getName() { return "clear_voting_config_exclusions_action"; } + @Override + public Set supportedCapabilities() { + return Set.of(PLAIN_TEXT_RESPONSE_CAPABILITY_NAME); + } + @Override protected RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException { final var req = resolveVotingConfigExclusionsRequest(request); diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestDeleteDesiredBalanceAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestDeleteDesiredBalanceAction.java index aff57e62dea9c..85bc37b510f79 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestDeleteDesiredBalanceAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestDeleteDesiredBalanceAction.java @@ -21,6 +21,9 @@ import java.io.IOException; import java.util.List; +import java.util.Set; + +import static org.elasticsearch.rest.action.EmptyResponseListener.PLAIN_TEXT_RESPONSE_CAPABILITY_NAME; @ServerlessScope(Scope.INTERNAL) public class RestDeleteDesiredBalanceAction extends BaseRestHandler { @@ -35,6 +38,11 @@ public List routes() { return List.of(new Route(RestRequest.Method.DELETE, "_internal/desired_balance")); } + @Override + public Set supportedCapabilities() { + return Set.of(PLAIN_TEXT_RESPONSE_CAPABILITY_NAME); + } + @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { final var req = new DesiredBalanceRequest(RestUtils.getMasterNodeTimeout(request)); diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestDeleteDesiredNodesAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestDeleteDesiredNodesAction.java index 0be3a728a7d7d..5fa058100e78b 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestDeleteDesiredNodesAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestDeleteDesiredNodesAction.java @@ -18,9 +18,11 @@ 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_RESPONSE_CAPABILITY_NAME; public class RestDeleteDesiredNodesAction extends BaseRestHandler { @Override @@ -33,6 +35,11 @@ public List routes() { return List.of(new Route(RestRequest.Method.DELETE, "_internal/desired_nodes")); } + @Override + public Set supportedCapabilities() { + return Set.of(PLAIN_TEXT_RESPONSE_CAPABILITY_NAME); + } + @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { final var deleteDesiredNodesRequest = new AcknowledgedRequest.Plain(getMasterNodeTimeout(request), getAckTimeout(request)); diff --git a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/DeprecationCacheResetAction.java b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/DeprecationCacheResetAction.java index 0f374efb520d5..e852ef2f26d77 100644 --- a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/DeprecationCacheResetAction.java +++ b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/DeprecationCacheResetAction.java @@ -67,7 +67,7 @@ public boolean equals(Object obj) { } } - public static class Response extends BaseNodesResponse implements Writeable, ToXContentObject { + public static class Response extends BaseNodesResponse implements Writeable { public Response(ClusterName clusterName, List nodes, List failures) { super(clusterName, nodes, failures); } @@ -82,11 +82,6 @@ protected void writeNodesTo(StreamOutput out, List nodes) { TransportAction.localOnly(); } - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) { - return builder; - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/RestDeprecationCacheResetAction.java b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/RestDeprecationCacheResetAction.java index 9184dceecbfe8..90dbc125bd445 100644 --- a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/RestDeprecationCacheResetAction.java +++ b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/RestDeprecationCacheResetAction.java @@ -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_RESPONSE_CAPABILITY_NAME; public class RestDeprecationCacheResetAction extends BaseRestHandler { @@ -30,8 +32,17 @@ public String getName() { } @Override - public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + public Set supportedCapabilities() { + return Set.of(PLAIN_TEXT_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) + ); } } From 2d2f8eae7ac158164f27e6abab5f9f67223dab54 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 25 Mar 2025 12:35:59 +0000 Subject: [PATCH 7/9] Missed one --- .../rest-api-spec/test/cluster.desired_balance/10_basic.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_balance/10_basic.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_balance/10_basic.yml index edb167ddbdf6e..9b8e4e83a82ef 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_balance/10_basic.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_balance/10_basic.yml @@ -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_response ] - do: _internal.delete_desired_balance: { } From 02b1bf314d17349f96bb9379a2f22e6574a01cef Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 4 Apr 2025 10:29:14 +0100 Subject: [PATCH 8/9] Rename capability --- .../rest-api-spec/test/cluster.desired_balance/10_basic.yml | 2 +- .../rest-api-spec/test/cluster.desired_nodes/10_basic.yml | 2 +- .../test/cluster.desired_nodes/11_old_format.yml | 2 +- .../rest-api-spec/test/cluster.desired_nodes/20_dry_run.yml | 2 +- .../test/cluster.voting_config_exclusions/10_basic.yml | 4 ++-- .../org/elasticsearch/rest/action/EmptyResponseListener.java | 2 +- .../admin/cluster/RestAddVotingConfigExclusionAction.java | 4 ++-- .../admin/cluster/RestClearVotingConfigExclusionsAction.java | 4 ++-- .../action/admin/cluster/RestDeleteDesiredBalanceAction.java | 4 ++-- .../action/admin/cluster/RestDeleteDesiredNodesAction.java | 4 ++-- .../deprecation/logging/RestDeprecationCacheResetAction.java | 4 ++-- 11 files changed, 17 insertions(+), 17 deletions(-) diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_balance/10_basic.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_balance/10_basic.yml index 9b8e4e83a82ef..82a086bdc3fa4 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_balance/10_basic.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_balance/10_basic.yml @@ -184,7 +184,7 @@ setup: capabilities: - method: DELETE path: /_internal/desired_balance - capabilities: [ plain_text_response ] + capabilities: [ plain_text_empty_response ] - do: _internal.delete_desired_balance: { } diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_nodes/10_basic.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_nodes/10_basic.yml index 97efac652ef7d..4a158928d985a 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_nodes/10_basic.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_nodes/10_basic.yml @@ -7,7 +7,7 @@ setup: capabilities: - method: DELETE path: /_internal/desired_nodes - capabilities: [ plain_text_response ] + capabilities: [ plain_text_empty_response ] --- teardown: - do: diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_nodes/11_old_format.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_nodes/11_old_format.yml index 637ae09d8508b..a9a2071a530cd 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_nodes/11_old_format.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_nodes/11_old_format.yml @@ -10,7 +10,7 @@ setup: capabilities: - method: DELETE path: /_internal/desired_nodes - capabilities: [ plain_text_response ] + capabilities: [ plain_text_empty_response ] --- teardown: - do: diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_nodes/20_dry_run.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_nodes/20_dry_run.yml index 83889eab62c66..ba07cd1dfc836 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_nodes/20_dry_run.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.desired_nodes/20_dry_run.yml @@ -7,7 +7,7 @@ setup: capabilities: - method: DELETE path: /_internal/desired_nodes - capabilities: [ plain_text_response ] + capabilities: [ plain_text_empty_response ] --- teardown: - do: diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.voting_config_exclusions/10_basic.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.voting_config_exclusions/10_basic.yml index 0db1c60e832f2..1794323245243 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.voting_config_exclusions/10_basic.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cluster.voting_config_exclusions/10_basic.yml @@ -4,10 +4,10 @@ setup: capabilities: - method: POST path: /_cluster/voting_config_exclusions - capabilities: [ plain_text_response ] + capabilities: [ plain_text_empty_response ] - method: DELETE path: /_cluster/voting_config_exclusions - capabilities: [ plain_text_response ] + capabilities: [ plain_text_empty_response ] reason: needs these capabilities --- diff --git a/server/src/main/java/org/elasticsearch/rest/action/EmptyResponseListener.java b/server/src/main/java/org/elasticsearch/rest/action/EmptyResponseListener.java index 29f81ad99612f..43f67c8dbbc4c 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/EmptyResponseListener.java +++ b/server/src/main/java/org/elasticsearch/rest/action/EmptyResponseListener.java @@ -34,5 +34,5 @@ public RestResponse buildResponse(ActionResponse.Empty ignored) throws Exception * 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_RESPONSE_CAPABILITY_NAME = "plain_text_response"; + public static final String PLAIN_TEXT_EMPTY_RESPONSE_CAPABILITY_NAME = "plain_text_empty_response"; } diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestAddVotingConfigExclusionAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestAddVotingConfigExclusionAction.java index 62142c5c795de..e6f15bbefb1c6 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestAddVotingConfigExclusionAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestAddVotingConfigExclusionAction.java @@ -24,7 +24,7 @@ import static org.elasticsearch.rest.RestRequest.Method.POST; import static org.elasticsearch.rest.RestUtils.getMasterNodeTimeout; -import static org.elasticsearch.rest.action.EmptyResponseListener.PLAIN_TEXT_RESPONSE_CAPABILITY_NAME; +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); @@ -41,7 +41,7 @@ public List routes() { @Override public Set supportedCapabilities() { - return Set.of(PLAIN_TEXT_RESPONSE_CAPABILITY_NAME); + return Set.of(PLAIN_TEXT_EMPTY_RESPONSE_CAPABILITY_NAME); } @Override diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClearVotingConfigExclusionsAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClearVotingConfigExclusionsAction.java index 0703b886ed3b1..2eee91d4e86f5 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClearVotingConfigExclusionsAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClearVotingConfigExclusionsAction.java @@ -22,7 +22,7 @@ import static org.elasticsearch.rest.RestRequest.Method.DELETE; import static org.elasticsearch.rest.RestUtils.getMasterNodeTimeout; -import static org.elasticsearch.rest.action.EmptyResponseListener.PLAIN_TEXT_RESPONSE_CAPABILITY_NAME; +import static org.elasticsearch.rest.action.EmptyResponseListener.PLAIN_TEXT_EMPTY_RESPONSE_CAPABILITY_NAME; public class RestClearVotingConfigExclusionsAction extends BaseRestHandler { @@ -43,7 +43,7 @@ public String getName() { @Override public Set supportedCapabilities() { - return Set.of(PLAIN_TEXT_RESPONSE_CAPABILITY_NAME); + return Set.of(PLAIN_TEXT_EMPTY_RESPONSE_CAPABILITY_NAME); } @Override diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestDeleteDesiredBalanceAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestDeleteDesiredBalanceAction.java index 85bc37b510f79..a432bf5e589c7 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestDeleteDesiredBalanceAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestDeleteDesiredBalanceAction.java @@ -23,7 +23,7 @@ import java.util.List; import java.util.Set; -import static org.elasticsearch.rest.action.EmptyResponseListener.PLAIN_TEXT_RESPONSE_CAPABILITY_NAME; +import static org.elasticsearch.rest.action.EmptyResponseListener.PLAIN_TEXT_EMPTY_RESPONSE_CAPABILITY_NAME; @ServerlessScope(Scope.INTERNAL) public class RestDeleteDesiredBalanceAction extends BaseRestHandler { @@ -40,7 +40,7 @@ public List routes() { @Override public Set supportedCapabilities() { - return Set.of(PLAIN_TEXT_RESPONSE_CAPABILITY_NAME); + return Set.of(PLAIN_TEXT_EMPTY_RESPONSE_CAPABILITY_NAME); } @Override diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestDeleteDesiredNodesAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestDeleteDesiredNodesAction.java index 5fa058100e78b..9f4b833017d5b 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestDeleteDesiredNodesAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestDeleteDesiredNodesAction.java @@ -22,7 +22,7 @@ import static org.elasticsearch.rest.RestUtils.getAckTimeout; import static org.elasticsearch.rest.RestUtils.getMasterNodeTimeout; -import static org.elasticsearch.rest.action.EmptyResponseListener.PLAIN_TEXT_RESPONSE_CAPABILITY_NAME; +import static org.elasticsearch.rest.action.EmptyResponseListener.PLAIN_TEXT_EMPTY_RESPONSE_CAPABILITY_NAME; public class RestDeleteDesiredNodesAction extends BaseRestHandler { @Override @@ -37,7 +37,7 @@ public List routes() { @Override public Set supportedCapabilities() { - return Set.of(PLAIN_TEXT_RESPONSE_CAPABILITY_NAME); + return Set.of(PLAIN_TEXT_EMPTY_RESPONSE_CAPABILITY_NAME); } @Override diff --git a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/RestDeprecationCacheResetAction.java b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/RestDeprecationCacheResetAction.java index 90dbc125bd445..7db2a6b3a8c03 100644 --- a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/RestDeprecationCacheResetAction.java +++ b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/RestDeprecationCacheResetAction.java @@ -17,7 +17,7 @@ import java.util.Set; import static org.elasticsearch.rest.RestRequest.Method.DELETE; -import static org.elasticsearch.rest.action.EmptyResponseListener.PLAIN_TEXT_RESPONSE_CAPABILITY_NAME; +import static org.elasticsearch.rest.action.EmptyResponseListener.PLAIN_TEXT_EMPTY_RESPONSE_CAPABILITY_NAME; public class RestDeprecationCacheResetAction extends BaseRestHandler { @@ -33,7 +33,7 @@ public String getName() { @Override public Set supportedCapabilities() { - return Set.of(PLAIN_TEXT_RESPONSE_CAPABILITY_NAME); + return Set.of(PLAIN_TEXT_EMPTY_RESPONSE_CAPABILITY_NAME); } @Override From 1f3c09f44f3515b1ebaa8c34a962ed9f8e9a91b8 Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 4 Apr 2025 10:31:05 +0100 Subject: [PATCH 9/9] Fix up operator privileges test --- .../test/mixed_cluster/130_operator_privileges.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/130_operator_privileges.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/130_operator_privileges.yml index 2b5c3b418eb84..4fa34dac9e33e 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/130_operator_privileges.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/130_operator_privileges.yml @@ -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: