From e7b338a077a1ad3c3cc67c1c425adfea5e910734 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Wed, 2 Sep 2015 23:16:07 +0200 Subject: [PATCH] test: added smoke test for the shield tribe node integration Original commit: elastic/x-pack-elasticsearch@f7ab8b9044367d8041281c95a018d57bbb5e9e33 --- qa/pom.xml | 3 + .../integration-tests.xml | 166 ++++++++ qa/shield-tribe-node-tests/pom.xml | 202 ++++++++++ .../test/tribe_node/10_basic.yaml | 27 ++ qa/shield-tribe-node-tests/shield-roles.yml | 4 + .../org/elasticsearch/ant/HttpCondition.java | 25 ++ .../java/org/elasticsearch/ant/HttpTask.java | 82 ++++ .../java/org/elasticsearch/shield/RestIT.java | 44 +++ .../shield/TribeRestTestCase.java | 373 ++++++++++++++++++ .../shield/tribe/TribeTests.java | 230 ----------- 10 files changed, 926 insertions(+), 230 deletions(-) create mode 100644 qa/shield-tribe-node-tests/integration-tests.xml create mode 100644 qa/shield-tribe-node-tests/pom.xml create mode 100644 qa/shield-tribe-node-tests/rest-api-spec/test/tribe_node/10_basic.yaml create mode 100644 qa/shield-tribe-node-tests/shield-roles.yml create mode 100644 qa/shield-tribe-node-tests/src/test/java/org/elasticsearch/ant/HttpCondition.java create mode 100644 qa/shield-tribe-node-tests/src/test/java/org/elasticsearch/ant/HttpTask.java create mode 100644 qa/shield-tribe-node-tests/src/test/java/org/elasticsearch/shield/RestIT.java create mode 100644 qa/shield-tribe-node-tests/src/test/java/org/elasticsearch/shield/TribeRestTestCase.java delete mode 100644 shield/src/test/java/org/elasticsearch/shield/tribe/TribeTests.java diff --git a/qa/pom.xml b/qa/pom.xml index dd863df3092..3444a8d3f7c 100644 --- a/qa/pom.xml +++ b/qa/pom.xml @@ -302,6 +302,7 @@ 127.0.0.1:${integ.transport.port} + 127.0.0.1:${integ.http.port} @@ -314,10 +315,12 @@ smoke-test-plugins shield-core-rest-tests smoke-test-watcher-with-shield shield-example-realm + shield-tribe-node-tests diff --git a/qa/shield-tribe-node-tests/integration-tests.xml b/qa/shield-tribe-node-tests/integration-tests.xml new file mode 100644 index 00000000000..9f620b601b2 --- /dev/null +++ b/qa/shield-tribe-node-tests/integration-tests.xml @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Adding roles.yml + + + Adding shield users... + + + + + + + + + + + + Starting two nodes, each node in a different cluster + + + + + + Failed to start first cluster with message: ${failure.message} + + + + + + + + + + Failed to start second cluster with message: ${failure.message} + + + + + + + + + Starting a tribe node, configured to connect to cluster1 and cluster2 + + + + + + + + + + + + + Creating index1 in cluster1 + + + Creating index2 in cluster2 + + + + Failed to start tribe node with message: ${failure.message} + + + + + + + + + + + + + + + + + + + + diff --git a/qa/shield-tribe-node-tests/pom.xml b/qa/shield-tribe-node-tests/pom.xml new file mode 100644 index 00000000000..c1edce7e7bb --- /dev/null +++ b/qa/shield-tribe-node-tests/pom.xml @@ -0,0 +1,202 @@ + + + + + + 4.0.0 + + + org.elasticsearch.qa + x-plugins-qa + 3.0.0-SNAPSHOT + + + smoke-test-tribe-node-with-shield + QA: Smoke Test tribe node with Shield + Start a tribe node and two nodes both with a different cluster name and verifies if all data accessable via the tribe node + + + true + ${project.basedir}/integration-tests.xml + false + license,shield + + + + + org.elasticsearch.plugin + shield + ${elasticsearch.version} + test + + + org.elasticsearch.plugin + license + ${project.version} + test + + + + + + + + ${elasticsearch.tools.directory}/rest-api-spec + rest-api-spec + + + api/info.json + api/cluster.health.json + api/cluster.state.json + + api/index.json + api/get.json + api/delete.json + api/delete-by-query.json + api/bulk.json + api/update.json + api/search.json + api/indices.delete.json + api/indices.refresh.json + + + + ${basedir}/rest-api-spec + true + rest-api-spec + + api/*.json + test/**/*.yaml + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + integ-setup-dependencies + pre-integration-test + + copy + + + ${skip.integ.tests} + true + ${integ.deps}/plugins + + + + + org.elasticsearch.distribution.zip + elasticsearch + ${elasticsearch.version} + zip + true + ${integ.deps} + + + + + org.elasticsearch.plugin + license + ${elasticsearch.version} + zip + true + + + + org.elasticsearch.plugin + shield + ${elasticsearch.version} + zip + true + + + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + + integ-setup + pre-integration-test + + run + + + + + + + + + + + ${skip.integ.tests} + + + + + integ-teardown + post-integration-test + + run + + + + + + + ${skip.integ.tests} + + + + + + ant-contrib + ant-contrib + 1.0b3 + + + ant + ant + + + + + org.apache.ant + ant-nodeps + 1.8.1 + + + + + + + diff --git a/qa/shield-tribe-node-tests/rest-api-spec/test/tribe_node/10_basic.yaml b/qa/shield-tribe-node-tests/rest-api-spec/test/tribe_node/10_basic.yaml new file mode 100644 index 00000000000..277778a6dad --- /dev/null +++ b/qa/shield-tribe-node-tests/rest-api-spec/test/tribe_node/10_basic.yaml @@ -0,0 +1,27 @@ +--- +"Tribe node search": + + - do: + index: + index: index1 + type: test + id: 1 + body: { foo: bar } + + - do: + index: + index: index2 + type: test + id: 1 + body: { foo: bar } + + - do: + indices.refresh: {} + + - do: + search: + index: index1,index2 + body: + query: { term: { foo: bar }} + + - match: { hits.total: 2 } diff --git a/qa/shield-tribe-node-tests/shield-roles.yml b/qa/shield-tribe-node-tests/shield-roles.yml new file mode 100644 index 00000000000..261b0bcbdcd --- /dev/null +++ b/qa/shield-tribe-node-tests/shield-roles.yml @@ -0,0 +1,4 @@ +admin: + cluster: all + indices: + '*': all \ No newline at end of file diff --git a/qa/shield-tribe-node-tests/src/test/java/org/elasticsearch/ant/HttpCondition.java b/qa/shield-tribe-node-tests/src/test/java/org/elasticsearch/ant/HttpCondition.java new file mode 100644 index 00000000000..e687265f12f --- /dev/null +++ b/qa/shield-tribe-node-tests/src/test/java/org/elasticsearch/ant/HttpCondition.java @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.ant; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.taskdefs.condition.Condition; + +public class HttpCondition extends HttpTask implements Condition { + + private int expectedResponseCode = 200; + + @Override + public boolean eval() throws BuildException { + int responseCode = executeHttpRequest(); + getProject().log("response code=" + responseCode); + return responseCode == expectedResponseCode; + } + + public void setExpectedResponseCode(int expectedResponseCode) { + this.expectedResponseCode = expectedResponseCode; + } +} diff --git a/qa/shield-tribe-node-tests/src/test/java/org/elasticsearch/ant/HttpTask.java b/qa/shield-tribe-node-tests/src/test/java/org/elasticsearch/ant/HttpTask.java new file mode 100644 index 00000000000..3b25b4f28a9 --- /dev/null +++ b/qa/shield-tribe-node-tests/src/test/java/org/elasticsearch/ant/HttpTask.java @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.ant; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Task; +import org.elasticsearch.common.Base64; + +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +public class HttpTask extends Task { + + private String uri; + private String method; + private String body; + + private String username; + private String password; + + @Override + public void execute() throws BuildException { + int responseCode = executeHttpRequest(); + getProject().log("response code=" + responseCode); + } + + protected int executeHttpRequest() { + try { + URI uri = new URI(this.uri); + URL url = uri.toURL(); + getProject().log("url=" + url); + HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); + if (method != null) { + urlConnection.setRequestMethod(method); + } + if (username != null) { + String basicAuth = "Basic " + Base64.encodeBytes((username + ":" + password).getBytes(StandardCharsets.UTF_8)); + urlConnection.setRequestProperty("Authorization", basicAuth); + } + if (body != null) { + urlConnection.setDoOutput(true); + urlConnection.setRequestProperty("Accept-Charset", StandardCharsets.UTF_8.name()); + byte[] bytes = body.getBytes(StandardCharsets.UTF_8.name()); + urlConnection.setRequestProperty("Content-Length", String.valueOf(bytes.length)); + urlConnection.getOutputStream().write(bytes); + urlConnection.getOutputStream().close(); + } + urlConnection.connect(); + int responseCode = urlConnection.getResponseCode(); + urlConnection.disconnect(); + return responseCode; + } catch (Exception e) { + throw new BuildException(e); + } + } + + public void setUri(String uri) { + this.uri = uri; + } + + public void setMethod(String method) { + this.method = method; + } + + public void setBody(String body) { + this.body = body; + } + + public void setUsername(String username) { + this.username = username; + } + + public void setPassword(String password) { + this.password = password; + } + +} diff --git a/qa/shield-tribe-node-tests/src/test/java/org/elasticsearch/shield/RestIT.java b/qa/shield-tribe-node-tests/src/test/java/org/elasticsearch/shield/RestIT.java new file mode 100644 index 00000000000..46ef722c833 --- /dev/null +++ b/qa/shield-tribe-node-tests/src/test/java/org/elasticsearch/shield/RestIT.java @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.shield; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; + +import org.elasticsearch.client.support.Headers; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.shield.authc.support.SecuredString; +import org.elasticsearch.test.rest.ESRestTestCase; +import org.elasticsearch.test.rest.RestTestCandidate; +import org.elasticsearch.test.rest.parser.RestTestParseException; + +import java.io.IOException; + +import static org.elasticsearch.shield.authc.support.UsernamePasswordToken.basicAuthHeaderValue; + +public class RestIT extends TribeRestTestCase { + + private static final String USER = "test_admin"; + private static final String PASS = "changeme"; + + public RestIT(@Name("yaml") RestTestCandidate testCandidate) { + super(testCandidate); + } + + @ParametersFactory + public static Iterable parameters() throws IOException, RestTestParseException { + return ESRestTestCase.createParameters(0, 1); + } + + @Override + protected Settings restClientSettings() { + String token = basicAuthHeaderValue(USER, new SecuredString(PASS.toCharArray())); + return Settings.builder() + .put(Headers.PREFIX + ".Authorization", token) + .build(); + } +} + diff --git a/qa/shield-tribe-node-tests/src/test/java/org/elasticsearch/shield/TribeRestTestCase.java b/qa/shield-tribe-node-tests/src/test/java/org/elasticsearch/shield/TribeRestTestCase.java new file mode 100644 index 00000000000..5d2f3a9ce34 --- /dev/null +++ b/qa/shield-tribe-node-tests/src/test/java/org/elasticsearch/shield/TribeRestTestCase.java @@ -0,0 +1,373 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.shield; + +import com.carrotsearch.randomizedtesting.RandomizedTest; +import com.carrotsearch.randomizedtesting.annotations.TestGroup; +import com.carrotsearch.randomizedtesting.annotations.TimeoutSuite; +import org.apache.lucene.util.IOUtils; +import org.apache.lucene.util.LuceneTestCase.SuppressCodecs; +import org.apache.lucene.util.LuceneTestCase.SuppressFsync; +import org.apache.lucene.util.TimeUnits; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.SuppressForbidden; +import org.elasticsearch.common.io.PathUtils; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.InetSocketTransportAddress; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.node.Node; +import org.elasticsearch.repositories.uri.URLRepository; +import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.test.ESIntegTestCase.ClusterScope; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.rest.ESRestTestCase; +import org.elasticsearch.test.rest.RestTestCandidate; +import org.elasticsearch.test.rest.RestTestExecutionContext; +import org.elasticsearch.test.rest.client.RestException; +import org.elasticsearch.test.rest.parser.RestTestParseException; +import org.elasticsearch.test.rest.parser.RestTestSuiteParser; +import org.elasticsearch.test.rest.section.DoSection; +import org.elasticsearch.test.rest.section.ExecutableSection; +import org.elasticsearch.test.rest.section.RestTestSuite; +import org.elasticsearch.test.rest.section.SkipSection; +import org.elasticsearch.test.rest.section.TestSection; +import org.elasticsearch.test.rest.spec.RestApi; +import org.elasticsearch.test.rest.spec.RestSpec; +import org.elasticsearch.test.rest.support.FileUtils; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.net.*; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Forked from RestTestCase with changes required to run rest tests via a tribe node + * + * Reasons for forking: + * 1) Always communicate via the tribe node from the tests. The original class in core connects to any endpoint it can see via the nodes info api and that would mean also the nodes part of the other clusters would be just as entry point. This should not happen for the tribe tests + * 2) The original class in core executes delete calls after each test, but the tribe node can't handle master level write operations. These api calls hang for 1m and then just fail. + * 3) The indices in cluster1 and cluster2 are created from the ant integ file and because of that the original class in core would just remove that in between tests. + * 4) extends ESTestCase instead if ESIntegTestCase and doesn't setup a test cluster and just connects to the one endpoint defined in the tests.rest.cluster. + */ +@ESRestTestCase.Rest +@SuppressFsync // we aren't trying to test this here, and it can make the test slow +@SuppressCodecs("*") // requires custom completion postings format +@ClusterScope(randomDynamicTemplates = false) +@TimeoutSuite(millis = 40 * TimeUnits.MINUTE) // timeout the suite after 40min and fail the test. +public abstract class TribeRestTestCase extends ESTestCase { + + /** + * Property that allows to control whether the REST tests are run (default) or not + */ + public static final String TESTS_REST = "tests.rest"; + + public static final String TESTS_REST_CLUSTER = "tests.rest.cluster"; + + /** + * Annotation for REST tests + */ + @Inherited + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @TestGroup(enabled = true, sysProperty = ESRestTestCase.TESTS_REST) + public @interface Rest { + } + + /** + * Property that allows to control which REST tests get run. Supports comma separated list of tests + * or directories that contain tests e.g. -Dtests.rest.suite=index,get,create/10_with_id + */ + public static final String REST_TESTS_SUITE = "tests.rest.suite"; + /** + * Property that allows to blacklist some of the REST tests based on a comma separated list of globs + * e.g. -Dtests.rest.blacklist=get/10_basic/* + */ + public static final String REST_TESTS_BLACKLIST = "tests.rest.blacklist"; + /** + * Property that allows to control whether spec validation is enabled or not (default true). + */ + public static final String REST_TESTS_VALIDATE_SPEC = "tests.rest.validate_spec"; + /** + * Property that allows to control where the REST spec files need to be loaded from + */ + public static final String REST_TESTS_SPEC = "tests.rest.spec"; + + public static final String REST_LOAD_PACKAGED_TESTS = "tests.rest.load_packaged"; + + private static final String DEFAULT_TESTS_PATH = "/rest-api-spec/test"; + private static final String DEFAULT_SPEC_PATH = "/rest-api-spec/api"; + + private static final String PATHS_SEPARATOR = ","; + + private final PathMatcher[] blacklistPathMatchers; + private static RestTestExecutionContext restTestExecutionContext; + + private final RestTestCandidate testCandidate; + + public TribeRestTestCase(RestTestCandidate testCandidate) { + this.testCandidate = testCandidate; + String[] blacklist = resolvePathsProperty(REST_TESTS_BLACKLIST, null); + if (blacklist != null) { + blacklistPathMatchers = new PathMatcher[blacklist.length]; + int i = 0; + for (String glob : blacklist) { + blacklistPathMatchers[i++] = PathUtils.getDefaultFileSystem().getPathMatcher("glob:" + glob); + } + } else { + blacklistPathMatchers = new PathMatcher[0]; + } + } + + @Override + protected void afterIfFailed(List errors) { + logger.info("Stash dump on failure [{}]", XContentHelper.toString(restTestExecutionContext.stash())); + super.afterIfFailed(errors); + } + + public static Iterable createParameters(int id, int count) throws IOException, RestTestParseException { + TestGroup testGroup = Rest.class.getAnnotation(TestGroup.class); + String sysProperty = TestGroup.Utilities.getSysProperty(Rest.class); + boolean enabled; + try { + enabled = RandomizedTest.systemPropertyAsBoolean(sysProperty, testGroup.enabled()); + } catch (IllegalArgumentException e) { + // Ignore malformed system property, disable the group if malformed though. + enabled = false; + } + if (!enabled) { + return new ArrayList<>(); + } + //parse tests only if rest test group is enabled, otherwise rest tests might not even be available on file system + List restTestCandidates = collectTestCandidates(id, count); + List objects = new ArrayList<>(); + for (RestTestCandidate restTestCandidate : restTestCandidates) { + objects.add(new Object[]{restTestCandidate}); + } + return objects; + } + + private static List collectTestCandidates(int id, int count) throws RestTestParseException, IOException { + List testCandidates = new ArrayList<>(); + FileSystem fileSystem = getFileSystem(); + // don't make a try-with, getFileSystem returns null + // ... and you can't close() the default filesystem + try { + String[] paths = resolvePathsProperty(REST_TESTS_SUITE, DEFAULT_TESTS_PATH); + Map> yamlSuites = FileUtils.findYamlSuites(fileSystem, DEFAULT_TESTS_PATH, paths); + RestTestSuiteParser restTestSuiteParser = new RestTestSuiteParser(); + //yaml suites are grouped by directory (effectively by api) + for (String api : yamlSuites.keySet()) { + List yamlFiles = new ArrayList<>(yamlSuites.get(api)); + for (Path yamlFile : yamlFiles) { + String key = api + yamlFile.getFileName().toString(); + if (mustExecute(key, id, count)) { + RestTestSuite restTestSuite = restTestSuiteParser.parse(api, yamlFile); + for (TestSection testSection : restTestSuite.getTestSections()) { + testCandidates.add(new RestTestCandidate(restTestSuite, testSection)); + } + } + } + } + } finally { + IOUtils.close(fileSystem); + } + + //sort the candidates so they will always be in the same order before being shuffled, for repeatability + Collections.sort(testCandidates, new Comparator() { + @Override + public int compare(RestTestCandidate o1, RestTestCandidate o2) { + return o1.getTestPath().compareTo(o2.getTestPath()); + } + }); + + return testCandidates; + } + + private static boolean mustExecute(String test, int id, int count) { + int hash = (int) (Math.abs((long)test.hashCode()) % count); + return hash == id; + } + + private static String[] resolvePathsProperty(String propertyName, String defaultValue) { + String property = System.getProperty(propertyName); + if (!Strings.hasLength(property)) { + return defaultValue == null ? null : new String[]{defaultValue}; + } else { + return property.split(PATHS_SEPARATOR); + } + } + + /** + * Returns a new FileSystem to read REST resources, or null if they + * are available from classpath. + */ + @SuppressForbidden(reason = "proper use of URL, hack around a JDK bug") + static FileSystem getFileSystem() throws IOException { + // REST suite handling is currently complicated, with lots of filtering and so on + // For now, to work embedded in a jar, return a ZipFileSystem over the jar contents. + URL codeLocation = FileUtils.class.getProtectionDomain().getCodeSource().getLocation(); + boolean loadPackaged = RandomizedTest.systemPropertyAsBoolean(REST_LOAD_PACKAGED_TESTS, true); + if (codeLocation.getFile().endsWith(".jar") && loadPackaged) { + try { + // hack around a bug in the zipfilesystem implementation before java 9, + // its checkWritable was incorrect and it won't work without write permissions. + // if we add the permission, it will open jars r/w, which is too scary! so copy to a safe r-w location. + Path tmp = Files.createTempFile(null, ".jar"); + try (InputStream in = codeLocation.openStream()) { + Files.copy(in, tmp, StandardCopyOption.REPLACE_EXISTING); + } + return FileSystems.newFileSystem(new URI("jar:" + tmp.toUri()), Collections.emptyMap()); + } catch (URISyntaxException e) { + throw new IOException("couldn't open zipfilesystem: ", e); + } + } else { + return null; + } + } + + @BeforeClass + public static void initExecutionContext() throws IOException, RestException { + String[] specPaths = resolvePathsProperty(REST_TESTS_SPEC, DEFAULT_SPEC_PATH); + RestSpec restSpec = null; + FileSystem fileSystem = getFileSystem(); + // don't make a try-with, getFileSystem returns null + // ... and you can't close() the default filesystem + try { + restSpec = RestSpec.parseFrom(fileSystem, DEFAULT_SPEC_PATH, specPaths); + } finally { + IOUtils.close(fileSystem); + } + validateSpec(restSpec); + restTestExecutionContext = new RestTestExecutionContext(restSpec); + } + + private static void validateSpec(RestSpec restSpec) { + boolean validateSpec = RandomizedTest.systemPropertyAsBoolean(REST_TESTS_VALIDATE_SPEC, true); + if (validateSpec) { + StringBuilder errorMessage = new StringBuilder(); + for (RestApi restApi : restSpec.getApis()) { + if (restApi.getMethods().contains("GET") && restApi.isBodySupported()) { + if (!restApi.getMethods().contains("POST")) { + errorMessage.append("\n- ").append(restApi.getName()).append(" supports GET with a body but doesn't support POST"); + } + } + } + if (errorMessage.length() > 0) { + throw new IllegalArgumentException(errorMessage.toString()); + } + } + } + + @AfterClass + public static void close() { + if (restTestExecutionContext != null) { + restTestExecutionContext.close(); + restTestExecutionContext = null; + } + } + + /** + * Used to obtain settings for the REST client that is used to send REST requests. + */ + protected Settings restClientSettings() { + return Settings.EMPTY; + } + + protected InetSocketAddress[] httpAddresses() { + String clusterAddresses = System.getProperty(TESTS_REST_CLUSTER); + String[] stringAddresses = clusterAddresses.split(","); + InetSocketAddress[] transportAddresses = new InetSocketAddress[stringAddresses.length]; + int i = 0; + for (String stringAddress : stringAddresses) { + String[] split = stringAddress.split(":"); + if (split.length < 2) { + throw new IllegalArgumentException("address [" + clusterAddresses + "] not valid"); + } + try { + transportAddresses[i++] = new InetSocketAddress(InetAddress.getByName(split[0]), Integer.valueOf(split[1])); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("port is not valid, expected number but was [" + split[1] + "]"); + } catch (UnknownHostException e) { + throw new IllegalArgumentException("unknown host [" + split[0] + "]", e); + } + } + return transportAddresses; + } + + @Before + public void reset() throws IOException, RestException { + //skip test if it matches one of the blacklist globs + for (PathMatcher blacklistedPathMatcher : blacklistPathMatchers) { + //we need to replace a few characters otherwise the test section name can't be parsed as a path on windows + String testSection = testCandidate.getTestSection().getName().replace("*", "").replace("\\", "/").replaceAll("\\s+/", "/").replace(":", "").trim(); + String testPath = testCandidate.getSuitePath() + "/" + testSection; + assumeFalse("[" + testCandidate.getTestPath() + "] skipped, reason: blacklisted", blacklistedPathMatcher.matches(PathUtils.get(testPath))); + } + //The client needs non static info to get initialized, therefore it can't be initialized in the before class + restTestExecutionContext.initClient(httpAddresses(), restClientSettings()); + restTestExecutionContext.clear(); + + //skip test if the whole suite (yaml file) is disabled + assumeFalse(buildSkipMessage(testCandidate.getSuitePath(), testCandidate.getSetupSection().getSkipSection()), + testCandidate.getSetupSection().getSkipSection().skip(restTestExecutionContext.esVersion())); + //skip test if test section is disabled + assumeFalse(buildSkipMessage(testCandidate.getTestPath(), testCandidate.getTestSection().getSkipSection()), + testCandidate.getTestSection().getSkipSection().skip(restTestExecutionContext.esVersion())); + } + + private static String buildSkipMessage(String description, SkipSection skipSection) { + StringBuilder messageBuilder = new StringBuilder(); + if (skipSection.isVersionCheck()) { + messageBuilder.append("[").append(description).append("] skipped, reason: [").append(skipSection.getReason()).append("] "); + } else { + messageBuilder.append("[").append(description).append("] skipped, reason: features ").append(skipSection.getFeatures()).append(" not supported"); + } + return messageBuilder.toString(); + } + + @Test + public void test() throws IOException { + //let's check that there is something to run, otherwise there might be a problem with the test section + if (testCandidate.getTestSection().getExecutableSections().size() == 0) { + throw new IllegalArgumentException("No executable sections loaded for [" + testCandidate.getTestPath() + "]"); + } + + if (!testCandidate.getSetupSection().isEmpty()) { + logger.info("start setup test [{}]", testCandidate.getTestPath()); + for (DoSection doSection : testCandidate.getSetupSection().getDoSections()) { + doSection.execute(restTestExecutionContext); + } + logger.info("end setup test [{}]", testCandidate.getTestPath()); + } + + restTestExecutionContext.clear(); + + for (ExecutableSection executableSection : testCandidate.getTestSection().getExecutableSections()) { + executableSection.execute(restTestExecutionContext); + } + } +} diff --git a/shield/src/test/java/org/elasticsearch/shield/tribe/TribeTests.java b/shield/src/test/java/org/elasticsearch/shield/tribe/TribeTests.java deleted file mode 100644 index 734e0b3ebf0..00000000000 --- a/shield/src/test/java/org/elasticsearch/shield/tribe/TribeTests.java +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.shield.tribe; - -import com.google.common.collect.ImmutableMap; -import org.apache.lucene.util.LuceneTestCase; -import org.elasticsearch.action.admin.cluster.health.ClusterHealthStatus; -import org.elasticsearch.client.support.Headers; -import org.elasticsearch.cluster.node.DiscoveryNode; -import org.elasticsearch.cluster.node.DiscoveryNodes; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.transport.InetSocketTransportAddress; -import org.elasticsearch.common.transport.TransportAddress; -import org.elasticsearch.shield.authc.support.UsernamePasswordToken; -import org.elasticsearch.shield.crypto.InternalCryptoService; -import org.elasticsearch.shield.transport.netty.ShieldNettyHttpServerTransport; -import org.elasticsearch.test.InternalTestCluster; -import org.elasticsearch.test.ShieldIntegTestCase; -import org.elasticsearch.test.ShieldSettingsSource; -import org.elasticsearch.transport.Transport; -import org.elasticsearch.tribe.TribeService; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.Test; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import static org.elasticsearch.test.InternalTestCluster.clusterName; -import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount; -import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures; -import static org.hamcrest.Matchers.*; - -@LuceneTestCase.AwaitsFix(bugUrl = "https://github.com/elastic/x-plugins/issues/551") -public class TribeTests extends ShieldIntegTestCase { - - //use known suite prefix since their threads are already ignored via ElasticsearchThreadFilter - public static final String SECOND_CLUSTER_NODE_PREFIX = SUITE_CLUSTER_NODE_PREFIX; - public static final String TRIBE_CLUSTER_NODE_PREFIX = "tribe_cluster_node_"; - - private static InternalTestCluster cluster2; - private static ShieldSettingsSource tribeSettingsSource; - private InternalTestCluster tribeNodeCluster; - - @Before - public void setupSecondClusterAndTribeNode() throws Exception { - final Settings globalClusterSettings = internalCluster().getInstance(Settings.class); - - //TODO tribe nodes and all of the tribes need to have either ssl disabled or enabled as a whole - //we read the randomized setting from the global cluster and apply it to the other cluster that we are going to start - //for simplicity the same certificates are used on all clusters - final boolean sslTransportEnabled = globalClusterSettings.getAsBoolean("shield.transport.ssl", null); - - //we run this part in @Before instead of beforeClass because we need to have the current cluster already assigned to global - //so that we can retrieve its settings and apply some of them the the second cluster (and tribe node too) - if (cluster2 == null) { - // create another cluster - String cluster2Name = clusterName(Scope.SUITE.name(), randomLong()); - //no port conflicts as this test uses the global cluster and a suite cluster that gets manually created - ShieldSettingsSource cluster2SettingsSource = new ShieldSettingsSource(2, sslTransportEnabled, systemKey(), createTempDir(), Scope.SUITE); - cluster2 = new InternalTestCluster("network", randomLong(), createTempDir(), 2, 2, cluster2Name, cluster2SettingsSource, 0, false, SECOND_CLUSTER_NODE_PREFIX, true); - - assert tribeSettingsSource == null; - //given the low (2 and 1) number of nodes that the 2 SUITE clusters will have, we are not going to have port conflicts - tribeSettingsSource = new ShieldSettingsSource(1, sslTransportEnabled, systemKey(), createTempDir(), Scope.SUITE) { - @Override - public Settings nodeSettings(int nodeOrdinal) { - Settings shieldSettings = super.nodeSettings(nodeOrdinal); - //all the settings are needed for the tribe node, some of them will also need to be copied to the tribe clients configuration - Settings.Builder builder = Settings.builder().put(shieldSettings); - //the tribe node itself won't join any cluster, no need for unicast discovery configuration - builder.remove("discovery.type"); - builder.remove("discovery.zen.ping.multicast.enabled"); - //remove doesn't remove all the elements of an array, but we know it has only one element - builder.remove("discovery.zen.ping.unicast.hosts.0"); - - //copy the needed settings to the tribe clients configuration - ImmutableMap shieldSettingsAsMap = shieldSettings.getAsMap(); - for (Map.Entry entry : shieldSettingsAsMap.entrySet()) { - if (isSettingNeededForTribeClient(entry.getKey())) { - builder.put("tribe.t1." + entry.getKey(), entry.getValue()); - builder.put("tribe.t2." + entry.getKey(), entry.getValue()); - } - } - - return builder.put("tribe.t1.cluster.name", internalCluster().getClusterName()) - .putArray("tribe.t1.discovery.zen.ping.unicast.hosts", unicastHosts(internalCluster())) - .put("tribe.t1.shield.transport.ssl", sslTransportEnabled) - .put("tribe.t2.cluster.name", cluster2.getClusterName()) - .putArray("tribe.t2.discovery.zen.ping.unicast.hosts", unicastHosts(cluster2)) - .put("tribe.t2.shield.transport.ssl", sslTransportEnabled).build(); - } - - /** - * Returns true if the setting is needed to setup a tribe client and needs to get forwarded to it, false otherwise. - * Only some of the settings need to be forwarded e.g. realm configuration gets filtered out - */ - private boolean isSettingNeededForTribeClient(String settingKey) { - if (settingKey.equals("transport.host")) { - return true; - } - //discovery settings get forwarded to tribe clients to disable multicast discovery - if (settingKey.equals("discovery.type") || settingKey.equals("discovery.zen.ping.multicast.enabled")) { - return true; - } - //plugins need to be properly loaded on the tribe clients too - if (settingKey.startsWith("plugin")) { - return true; - } - //make sure node.mode is network on the tribe clients too - if (settingKey.equals("node.mode")) { - return true; - } - //forward the shield audit enabled to the tribe clients - if (settingKey.equals("shield.audit.enabled")) { - return true; - } - //forward the system key to the tribe clients, same file will be used - if (settingKey.equals(InternalCryptoService.FILE_SETTING)) { - return true; - } - //forward ssl settings to the tribe clients, same certificates will be used - if (settingKey.startsWith("shield.ssl") || settingKey.equals("shield.transport.ssl") || settingKey.equals(ShieldNettyHttpServerTransport.HTTP_SSL_SETTING)) { - return true; - } - //forward the credentials to the tribe clients - if (settingKey.equals("shield.user") || settingKey.equals(Headers.PREFIX + "." + UsernamePasswordToken.BASIC_AUTH_HEADER)) { - return true; - } - return false; - } - }; - } - - cluster2.beforeTest(getRandom(), 0.5); - - //we need to recreate the tribe node after each test otherwise ensureClusterSizeConsistency barfs - String tribeClusterName = clusterName(Scope.SUITE.name(), randomLong()); - tribeNodeCluster = new InternalTestCluster("network", randomLong(), createTempDir(), 1, 1, tribeClusterName, tribeSettingsSource, 0, false, TRIBE_CLUSTER_NODE_PREFIX, true); - tribeNodeCluster.beforeTest(getRandom(), 0.5); - awaitSameNodeCounts(); - } - - private static String[] unicastHosts(InternalTestCluster testCluster) { - Iterable transports = testCluster.getInstances(Transport.class); - List unicastHosts = new ArrayList<>(); - for (Transport transport : transports) { - TransportAddress transportAddress = transport.boundAddress().boundAddress(); - assertThat(transportAddress, is(instanceOf(InetSocketTransportAddress.class))); - InetSocketTransportAddress inetSocketTransportAddress = (InetSocketTransportAddress) transportAddress; - unicastHosts.add("localhost:" + inetSocketTransportAddress.address().getPort()); - } - return unicastHosts.toArray(new String[unicastHosts.size()]); - } - - @After - public void afterTest() throws IOException { - //we need to close the tribe node after each test otherwise ensureClusterSizeConsistency barfs - if (tribeNodeCluster != null) { - try { - tribeNodeCluster.close(); - } finally { - tribeNodeCluster = null; - } - } - //and clean up the second cluster that we manually started - if (cluster2 != null) { - try { - cluster2.wipe(); - } finally { - cluster2.afterTest(); - } - } - } - - @AfterClass - public static void tearDownSecondCluster() { - if (cluster2 != null) { - try { - cluster2.close(); - } finally { - cluster2 = null; - tribeSettingsSource = null; - } - } - } - - @Test - public void testIndexRefreshAndSearch() throws Exception { - internalCluster().client().admin().indices().prepareCreate("test1").get(); - cluster2.client().admin().indices().prepareCreate("test2").get(); - assertThat(tribeNodeCluster.client().admin().cluster().prepareHealth().setWaitForGreenStatus().get().getStatus(), equalTo(ClusterHealthStatus.GREEN)); - - tribeNodeCluster.client().prepareIndex("test1", "type1", "1").setSource("field1", "value1").get(); - tribeNodeCluster.client().prepareIndex("test2", "type1", "1").setSource("field1", "value1").get(); - assertNoFailures(tribeNodeCluster.client().admin().indices().prepareRefresh().get()); - - assertHitCount(tribeNodeCluster.client().prepareSearch().get(), 2l); - } - - private void awaitSameNodeCounts() throws Exception { - assertBusy(new Runnable() { - @Override - public void run() { - DiscoveryNodes tribeNodes = tribeNodeCluster.client().admin().cluster().prepareState().get().getState().getNodes(); - assertThat(countDataNodesForTribe("t1", tribeNodes), equalTo(internalCluster().client().admin().cluster().prepareState().get().getState().getNodes().dataNodes().size())); - assertThat(countDataNodesForTribe("t2", tribeNodes), equalTo(cluster2.client().admin().cluster().prepareState().get().getState().getNodes().dataNodes().size())); - } - }); - } - - private int countDataNodesForTribe(String tribeName, DiscoveryNodes nodes) { - int count = 0; - for (DiscoveryNode node : nodes) { - if (!node.dataNode()) { - continue; - } - if (tribeName.equals(node.getAttributes().get(TribeService.TRIBE_NAME))) { - count++; - } - } - return count; - } -}