diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/capabilities.json b/rest-api-spec/src/main/resources/rest-api-spec/api/capabilities.json index a96be0d63834e..f0537eee575d2 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/capabilities.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/capabilities.json @@ -41,6 +41,11 @@ "capabilities": { "type": "string", "description": "Comma-separated list of arbitrary API capabilities to check" + }, + "local_only": { + "type": "boolean", + "description": "True if only the node being called should be considered", + "visibility": "private" } } } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/capabilities/NodesCapabilitiesRequest.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/capabilities/NodesCapabilitiesRequest.java index c69d273727238..7adcff9f19ccb 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/capabilities/NodesCapabilitiesRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/capabilities/NodesCapabilitiesRequest.java @@ -24,10 +24,15 @@ public class NodesCapabilitiesRequest extends BaseNodesRequest getFeatures() { - return Set.of(RestNodesCapabilitiesAction.CAPABILITIES_ACTION, UNIFIED_HIGHLIGHTER_MATCHED_FIELDS); + return Set.of( + RestNodesCapabilitiesAction.CAPABILITIES_ACTION, + RestNodesCapabilitiesAction.LOCAL_ONLY_CAPABILITIES, + UNIFIED_HIGHLIGHTER_MATCHED_FIELDS + ); } @Override diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestNodesCapabilitiesAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestNodesCapabilitiesAction.java index 2bf389c2c2849..3ec2561cd5a65 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestNodesCapabilitiesAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestNodesCapabilitiesAction.java @@ -30,6 +30,7 @@ public class RestNodesCapabilitiesAction extends BaseRestHandler { public static final NodeFeature CAPABILITIES_ACTION = new NodeFeature("rest.capabilities_action"); + public static final NodeFeature LOCAL_ONLY_CAPABILITIES = new NodeFeature("rest.local_only_capabilities"); @Override public List routes() { @@ -38,7 +39,7 @@ public List routes() { @Override public Set supportedQueryParameters() { - return Set.of("timeout", "method", "path", "parameters", "capabilities"); + return Set.of("timeout", "method", "path", "parameters", "capabilities", "local_only"); } @Override @@ -48,7 +49,11 @@ public String getName() { @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - NodesCapabilitiesRequest r = new NodesCapabilitiesRequest().timeout(getTimeout(request)) + NodesCapabilitiesRequest requestNodes = request.paramAsBoolean("local_only", false) + ? new NodesCapabilitiesRequest(client.getLocalNodeId()) + : new NodesCapabilitiesRequest(); + + NodesCapabilitiesRequest r = requestNodes.timeout(getTimeout(request)) .method(RestRequest.Method.valueOf(request.param("method", "GET"))) .path(URLDecoder.decode(request.param("path"), StandardCharsets.UTF_8)) .parameters(request.paramAsStringArray("parameters", Strings.EMPTY_ARRAY)) diff --git a/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/ClientYamlTestExecutionContext.java b/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/ClientYamlTestExecutionContext.java index 4954065369ad9..213b43291abd8 100644 --- a/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/ClientYamlTestExecutionContext.java +++ b/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/ClientYamlTestExecutionContext.java @@ -15,10 +15,12 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.lucene.util.BytesRef; +import org.elasticsearch.client.Node; import org.elasticsearch.client.NodeSelector; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.util.Maps; +import org.elasticsearch.rest.action.admin.cluster.RestNodesCapabilitiesAction; import org.elasticsearch.test.rest.Stash; import org.elasticsearch.test.rest.TestFeatureService; import org.elasticsearch.test.rest.yaml.restspec.ClientYamlSuiteRestApi; @@ -280,8 +282,14 @@ public boolean clusterHasFeature(String featureId) { return testFeatureService.clusterHasFeature(featureId); } - public Optional clusterHasCapabilities(String method, String path, String parametersString, String capabilitiesString) { - Map params = Maps.newMapWithExpectedSize(5); + public Optional clusterHasCapabilities( + String method, + String path, + String parametersString, + String capabilitiesString, + boolean any + ) { + Map params = Maps.newMapWithExpectedSize(6); params.put("method", method); params.put("path", path); if (Strings.hasLength(parametersString)) { @@ -291,8 +299,46 @@ public Optional clusterHasCapabilities(String method, String path, Stri params.put("capabilities", capabilitiesString); } params.put("error_trace", "false"); // disable error trace + + if (clusterHasFeature(RestNodesCapabilitiesAction.LOCAL_ONLY_CAPABILITIES.id()) == false) { + // can only check the whole cluster + if (any) { + logger.warn( + "Cluster does not support checking individual nodes for capabilities," + + "check for [{} {}?{} {}] may be incorrect in mixed-version clusters", + method, + path, + parametersString, + capabilitiesString + ); + } + return checkCapability(NodeSelector.ANY, params); + } else { + // check each node individually - we can actually check any here + params.put("local_only", "true"); // we're calling each node individually + + // individually call each node, so we can control whether we do an 'any' or 'all' check + List nodes = clientYamlTestClient.getRestClient(NodeSelector.ANY).getNodes(); + + for (Node n : nodes) { + Optional nodeResult = checkCapability(new SpecificNodeSelector(n), params); + if (nodeResult.isEmpty()) { + return Optional.empty(); + } else if (any == nodeResult.get()) { + // either any == true and node has cap, + // or any == false (ie all) and this node does not have cap + return nodeResult; + } + } + + // if we got here, either any is true and no node has it, or any == false and all nodes have it + return Optional.of(any == false); + } + } + + private Optional checkCapability(NodeSelector nodeSelector, Map params) { try { - ClientYamlTestResponse resp = callApi("capabilities", params, emptyList(), emptyMap()); + ClientYamlTestResponse resp = callApi("capabilities", params, emptyList(), emptyMap(), nodeSelector); // anything other than 200 should result in an exception, handled below assert resp.getStatusCode() == 200 : "Unknown response code " + resp.getStatusCode(); return Optional.ofNullable(resp.evaluate("supported")); @@ -305,4 +351,17 @@ public Optional clusterHasCapabilities(String method, String path, Stri throw new UncheckedIOException(ioException); } } + + private record SpecificNodeSelector(Node node) implements NodeSelector { + @Override + public void select(Iterable nodes) { + // between getting the list of nodes, and checking here, the thing that is consistent is the host + // which becomes one of the bound addresses + for (var it = nodes.iterator(); it.hasNext();) { + if (it.next().getBoundHosts().contains(node.getHost()) == false) { + it.remove(); + } + } + } + } } diff --git a/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/Prerequisites.java b/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/Prerequisites.java index 96b5aff5d7047..16fc2769d3732 100644 --- a/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/Prerequisites.java +++ b/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/Prerequisites.java @@ -55,18 +55,18 @@ static Predicate skipOnKnownIssue(List checks) { // requirement not fulfilled if unknown / capabilities API not supported - return context -> checks.stream().allMatch(check -> checkCapabilities(context, check).orElse(false)); + return context -> checks.stream().allMatch(check -> checkCapabilities(context, check, false).orElse(false)); } static CapabilitiesPredicate skipCapabilities(List checks) { // skip if unknown / capabilities API not supported - return context -> checks.stream().anyMatch(check -> checkCapabilities(context, check).orElse(true)); + return context -> checks.stream().anyMatch(check -> checkCapabilities(context, check, true).orElse(true)); } interface CapabilitiesPredicate extends Predicate {} - private static Optional checkCapabilities(ClientYamlTestExecutionContext context, CapabilitiesCheck check) { - Optional b = context.clusterHasCapabilities(check.method(), check.path(), check.parameters(), check.capabilities()); + private static Optional checkCapabilities(ClientYamlTestExecutionContext context, CapabilitiesCheck check, boolean any) { + Optional b = context.clusterHasCapabilities(check.method(), check.path(), check.parameters(), check.capabilities(), any); return b; } } diff --git a/test/yaml-rest-runner/src/test/java/org/elasticsearch/test/rest/yaml/section/PrerequisiteSectionTests.java b/test/yaml-rest-runner/src/test/java/org/elasticsearch/test/rest/yaml/section/PrerequisiteSectionTests.java index cc27b7fc20b76..2522862135212 100644 --- a/test/yaml-rest-runner/src/test/java/org/elasticsearch/test/rest/yaml/section/PrerequisiteSectionTests.java +++ b/test/yaml-rest-runner/src/test/java/org/elasticsearch/test/rest/yaml/section/PrerequisiteSectionTests.java @@ -36,6 +36,7 @@ import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.oneOf; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -596,17 +597,17 @@ public void testEvaluateCapabilities() { assertTrue(section.skipCriteriaMet(context)); // always skip if unavailable assertFalse(section.requiresCriteriaMet(context)); // always fail requirements / skip if unavailable - when(context.clusterHasCapabilities(anyString(), anyString(), any(), any())).thenReturn(Optional.of(FALSE)); + when(context.clusterHasCapabilities(anyString(), anyString(), any(), any(), anyBoolean())).thenReturn(Optional.of(FALSE)); assertFalse(section.skipCriteriaMet(context)); assertFalse(section.requiresCriteriaMet(context)); - when(context.clusterHasCapabilities("GET", "/s", null, "c1,c2")).thenReturn(Optional.of(TRUE)); + when(context.clusterHasCapabilities("GET", "/s", null, "c1,c2", true)).thenReturn(Optional.of(TRUE)); assertTrue(section.skipCriteriaMet(context)); - when(context.clusterHasCapabilities("GET", "/r", null, null)).thenReturn(Optional.of(TRUE)); + when(context.clusterHasCapabilities("GET", "/r", null, null, false)).thenReturn(Optional.of(TRUE)); assertFalse(section.requiresCriteriaMet(context)); - when(context.clusterHasCapabilities("GET", "/r", "p1", null)).thenReturn(Optional.of(TRUE)); + when(context.clusterHasCapabilities("GET", "/r", "p1", null, false)).thenReturn(Optional.of(TRUE)); assertTrue(section.requiresCriteriaMet(context)); }