Skip to content

Commit

Permalink
Expose capability checks for YAML REST tests (#108425)
Browse files Browse the repository at this point in the history
Co-authored-by: Simon Cooper <simon.cooper@elastic.co>
  • Loading branch information
mosche and thecoop committed May 10, 2024
1 parent 2e0f8d0 commit d2d1357
Show file tree
Hide file tree
Showing 8 changed files with 307 additions and 24 deletions.
@@ -0,0 +1,47 @@
{
"capabilities": {
"documentation": {
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/master/capabilities.html",
"description": "Checks if the specified combination of method, API, parameters, and arbitrary capabilities are supported"
},
"stability": "experimental",
"visibility": "private",
"headers": {
"accept": [
"application/json"
]
},
"url": {
"paths": [
{
"path": "/_capabilities",
"methods": [
"GET"
]
}
]
},
"params": {
"method": {
"type": "enum",
"description": "REST method to check",
"options": [
"GET", "HEAD", "POST", "PUT", "DELETE"
],
"default": "GET"
},
"path": {
"type": "string",
"description": "API path to check"
},
"parameters": {
"type": "string",
"description": "Comma-separated list of API parameters to check"
},
"capabilities": {
"type": "string",
"description": "Comma-separated list of arbitrary API capabilities to check"
}
}
}
}
@@ -0,0 +1,28 @@
---
"Capabilities API":

- requires:
capabilities:
- method: GET
path: /_capabilities
parameters: [method, path, parameters, capabilities]
capabilities: []
reason: "capabilities api requires itself to be supported"

- do:
capabilities:
method: GET
path: /_capabilities
parameters: method,path,parameters,capabilities
error_trace: false

- match: { supported: true }

- do:
capabilities:
method: GET
path: /_capabilities
parameters: unknown
error_trace: false

- match: { supported: false }
Expand Up @@ -15,8 +15,8 @@

import java.io.IOException;

import static org.elasticsearch.test.hamcrest.OptionalMatchers.isPresentWith;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;

@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 0)
public class SimpleNodesCapabilitiesIT extends ESIntegTestCase {
Expand All @@ -31,25 +31,25 @@ public void testNodesCapabilities() throws IOException {
NodesCapabilitiesResponse response = clusterAdmin().nodesCapabilities(new NodesCapabilitiesRequest().path("_capabilities"))
.actionGet();
assertThat(response.getNodes(), hasSize(2));
assertThat(response.isSupported(), is(true));
assertThat(response.isSupported(), isPresentWith(true));

// check we support some parameters of the capabilities API
response = clusterAdmin().nodesCapabilities(new NodesCapabilitiesRequest().path("_capabilities").parameters("method", "path"))
.actionGet();
assertThat(response.getNodes(), hasSize(2));
assertThat(response.isSupported(), is(true));
assertThat(response.isSupported(), isPresentWith(true));

// check we don't support some other parameters of the capabilities API
response = clusterAdmin().nodesCapabilities(new NodesCapabilitiesRequest().path("_capabilities").parameters("method", "invalid"))
.actionGet();
assertThat(response.getNodes(), hasSize(2));
assertThat(response.isSupported(), is(false));
assertThat(response.isSupported(), isPresentWith(false));

// check we don't support a random invalid api
// TODO this is not working yet - see https://github.com/elastic/elasticsearch/issues/107425
/*response = clusterAdmin().nodesCapabilities(new NodesCapabilitiesRequest().path("_invalid"))
.actionGet();
assertThat(response.getNodes(), hasSize(2));
assertThat(response.isSupported(), is(false));*/
assertThat(response.isSupported(), isPresentWith(false));*/
}
}
Expand Up @@ -19,6 +19,7 @@

import java.io.IOException;
import java.util.List;
import java.util.Optional;

public class NodesCapabilitiesResponse extends BaseNodesResponse<NodeCapability> implements ToXContentFragment {
protected NodesCapabilitiesResponse(ClusterName clusterName, List<NodeCapability> nodes, List<FailedNodeException> failures) {
Expand All @@ -35,12 +36,15 @@ protected void writeNodesTo(StreamOutput out, List<NodeCapability> nodes) throws
TransportAction.localOnly();
}

public boolean isSupported() {
return getNodes().isEmpty() == false && getNodes().stream().allMatch(NodeCapability::isSupported);
public Optional<Boolean> isSupported() {
// if there are any failures, we don't know if it is fully supported by all nodes in the cluster
if (hasFailures() || getNodes().isEmpty()) return Optional.empty();
return Optional.of(getNodes().stream().allMatch(NodeCapability::isSupported));
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return builder.field("supported", isSupported());
Optional<Boolean> supported = isSupported();
return builder.field("supported", supported.orElse(null));
}
}
Expand Up @@ -16,7 +16,9 @@
import org.apache.logging.log4j.Logger;
import org.apache.lucene.util.BytesRef;
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.test.rest.Stash;
import org.elasticsearch.test.rest.TestFeatureService;
import org.elasticsearch.test.rest.yaml.restspec.ClientYamlSuiteRestApi;
Expand All @@ -25,14 +27,19 @@
import org.elasticsearch.xcontent.XContentType;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiPredicate;

import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;

/**
* Execution context passed across the REST tests.
* Holds the REST client used to communicate with elasticsearch.
Expand Down Expand Up @@ -122,7 +129,15 @@ public ClientYamlTestResponse callApi(
) throws IOException {
// makes a copy of the parameters before modifying them for this specific request
Map<String, String> requestParams = new HashMap<>(params);
requestParams.putIfAbsent("error_trace", "true"); // By default ask for error traces, this my be overridden by params
requestParams.compute("error_trace", (k, v) -> {
if (v == null) {
return "true"; // By default ask for error traces, this my be overridden by params
} else if (v.equals("false")) {
return null;
} else {
return v;
}
});
for (Map.Entry<String, String> entry : requestParams.entrySet()) {
if (stash.containsStashedValue(entry.getValue())) {
entry.setValue(stash.getValue(entry.getValue()).toString());
Expand Down Expand Up @@ -264,4 +279,30 @@ public ClientYamlTestCandidate getClientYamlTestCandidate() {
public boolean clusterHasFeature(String featureId) {
return testFeatureService.clusterHasFeature(featureId);
}

public Optional<Boolean> clusterHasCapabilities(String method, String path, String parametersString, String capabilitiesString) {
Map<String, String> params = Maps.newMapWithExpectedSize(5);
params.put("method", method);
params.put("path", path);
if (Strings.hasLength(parametersString)) {
params.put("parameters", parametersString);
}
if (Strings.hasLength(capabilitiesString)) {
params.put("capabilities", capabilitiesString);
}
params.put("error_trace", "false"); // disable error trace
try {
ClientYamlTestResponse resp = callApi("capabilities", params, emptyList(), emptyMap());
// 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"));
} catch (ClientYamlTestResponseException responseException) {
if (responseException.getRestTestResponse().getStatusCode() / 100 == 4) {
return Optional.empty(); // we don't know, the capabilities API is unsupported
}
throw new UncheckedIOException(responseException);
} catch (IOException ioException) {
throw new UncheckedIOException(ioException);
}
}
}

0 comments on commit d2d1357

Please sign in to comment.