Shuffle SQL's integration tests some (elastic/x-pack-elasticsearch#2403)

This shuffles all of SQL's QA tests into the `qa/sql` directory, moving
some shared resources into the new `qa:sql` project. It also rigs up
testing of the rest SQL interface in all the sql qa configurations:
without security, with security, and against multiple nodes.

I've had to make some modifications to how we handle the audit log
because it has gotten pretty slow. If these modifications turn out to
not be fast enough then I'll change the test to querying the log files
and drop the audit log index entirely but the index seems to be holding
out for now.

Original commit: elastic/x-pack-elasticsearch@ff3b5a74c1
This commit is contained in:
Nik Everett 2017-08-30 17:22:46 -04:00 committed by GitHub
parent c540e1d583
commit 97ddee8d1b
14 changed files with 219 additions and 118 deletions

View File

@ -53,13 +53,15 @@ case $key in
"-psql"
"check"
":x-pack-elasticsearch:plugin:precommit"
":x-pack-elasticsearch:qa:sql-multinode:check"
":x-pack-elasticsearch:qa:sql-no-security:check"
":x-pack-elasticsearch:qa:sql-security:check"
":x-pack-elasticsearch:qa:sql:check"
":x-pack-elasticsearch:qa:sql:multinode:check"
":x-pack-elasticsearch:qa:sql:no-security:check"
":x-pack-elasticsearch:qa:sql:security:check"
"-xforbiddenPatterns"
"-x:x-pack-elasticsearch:plugin:forbiddenPatterns"
"-x:x-pack-elasticsearch:qa:sql-no-security:forbiddenPatterns"
"-x:x-pack-elasticsearch:qa:sql-security:forbiddenPatterns"
"-x:x-pack-elasticsearch:qa:sql:forbiddenPatterns"
"-x:x-pack-elasticsearch:qa:sql:no-security:forbiddenPatterns"
"-x:x-pack-elasticsearch:qa:sql:security:forbiddenPatterns"
)
;;
jdk9)

View File

@ -1,21 +0,0 @@
import org.elasticsearch.gradle.test.RunTask
apply plugin: 'elasticsearch.standalone-rest-test'
apply plugin: 'elasticsearch.rest-test'
dependencies {
testCompile project(path: ':x-pack-elasticsearch:plugin', configuration: 'runtime')
testCompile project(path: ':x-pack-elasticsearch:plugin', configuration: 'testArtifacts')
testCompile project(path: ':modules:reindex')
}
// NOCOMMIT we should try this on multiple nodes
integTestCluster {
distribution = 'zip' // NOCOMMIT make double sure we want all the modules
numNodes = 2
plugin ':x-pack-elasticsearch:plugin'
setting 'xpack.ml.enabled', 'false'
setting 'xpack.monitoring.enabled', 'false'
setting 'xpack.security.enabled', 'false'
}

View File

@ -1,28 +0,0 @@
import org.elasticsearch.gradle.test.RunTask
apply plugin: 'elasticsearch.standalone-rest-test'
apply plugin: 'elasticsearch.rest-test'
integTestCluster {
distribution = 'zip' // NOCOMMIT make double sure we want all the modules
plugin project(':x-pack-elasticsearch:plugin').path
/* Get a "clean" test without the other x-pack features here and check them
* all together later on. */
setting 'xpack.security.enabled', 'false'
setting 'xpack.monitoring.enabled', 'false'
setting 'xpack.ml.enabled', 'false'
setting 'xpack.watcher.enabled', 'false'
setting 'script.max_compilations_per_minute', '1000'
}
task run(type: RunTask) {
distribution = 'zip' // NOCOMMIT make double sure we want all the modules
plugin project(':x-pack-elasticsearch:plugin').path
/* Get a "clean" test without the other x-pack features here and check them
* all together later on. */
setting 'xpack.security.enabled', 'false'
setting 'xpack.monitoring.enabled', 'false'
setting 'xpack.ml.enabled', 'false'
setting 'xpack.watcher.enabled', 'false'
setting 'script.max_compilations_per_minute', '1000'
}

52
qa/sql/build.gradle Normal file
View File

@ -0,0 +1,52 @@
import org.elasticsearch.gradle.precommit.PrecommitTasks
import org.elasticsearch.gradle.test.RunTask
description = 'Integration tests for SQL'
apply plugin: 'elasticsearch.build'
dependencies {
compile project(path: ':x-pack-elasticsearch:plugin', configuration: 'runtime')
compile project(path: ':x-pack-elasticsearch:plugin', configuration: 'testArtifacts')
compile "org.elasticsearch.test:framework:${versions.elasticsearch}"
}
// the main files are actually test files, so use the appropriate forbidden api sigs
forbiddenApisMain {
signaturesURLs = [PrecommitTasks.getResource('/forbidden/es-all-signatures.txt'),
PrecommitTasks.getResource('/forbidden/es-test-signatures.txt')]
}
// just test utilities
test.enabled = false
dependencyLicenses.enabled = false
subprojects {
apply plugin: 'elasticsearch.standalone-rest-test'
apply plugin: 'elasticsearch.rest-test'
dependencies {
testCompile project(path: ':x-pack-elasticsearch:qa:sql')
testCompile project(path: ':x-pack-elasticsearch:plugin', configuration: 'runtime')
testCompile project(path: ':x-pack-elasticsearch:plugin', configuration: 'testArtifacts')
testCompile "org.elasticsearch.test:framework:${versions.elasticsearch}"
}
integTestCluster {
distribution = 'zip'
plugin project(':x-pack-elasticsearch:plugin').path
setting 'xpack.monitoring.enabled', 'false'
setting 'xpack.ml.enabled', 'false'
setting 'xpack.watcher.enabled', 'false'
setting 'script.max_compilations_per_minute', '1000'
}
task run(type: RunTask) {
distribution = 'zip'
plugin project(':x-pack-elasticsearch:plugin').path
setting 'xpack.monitoring.enabled', 'false'
setting 'xpack.ml.enabled', 'false'
setting 'xpack.watcher.enabled', 'false'
setting 'script.max_compilations_per_minute', '1000'
}
}

View File

@ -0,0 +1,4 @@
integTestCluster {
numNodes = 2
setting 'xpack.security.enabled', 'false'
}

View File

@ -0,0 +1,15 @@
/*
* 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.xpack.sql.multinode;
import org.elasticsearch.xpack.sql.RestSqlTestCase;
/**
* Integration test for the rest sql action. The one that speaks json directly to a
* user rather than to the JDBC driver or CLI.
*/
public class RestSqlIT extends RestSqlTestCase {
}

View File

@ -3,7 +3,7 @@
* 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.xpack.sql;
package org.elasticsearch.xpack.sql.multinode;
import org.apache.http.HttpHost;
import org.apache.http.entity.ContentType;
@ -26,7 +26,7 @@ import java.util.Map;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static java.util.Collections.unmodifiableMap;
import static org.elasticsearch.xpack.sql.RestSqlTestCase.columnInfo;
public class SqlMultinodeIT extends ESRestTestCase {
/**
@ -110,12 +110,4 @@ public class SqlMultinodeIT extends ESRestTestCase {
fail("Response does not match:\n" + message.toString());
}
}
private Map<String, Object> columnInfo(String name, String type) {
Map<String, Object> column = new HashMap<>();
column.put("name", name);
column.put("type", type);
return unmodifiableMap(column);
}
}

View File

@ -0,0 +1,7 @@
integTestCluster {
setting 'xpack.security.enabled', 'false'
}
run {
setting 'xpack.security.enabled', 'false'
}

View File

@ -0,0 +1,15 @@
/*
* 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.xpack.sql.nosecurity;
import org.elasticsearch.xpack.sql.RestSqlTestCase;
/**
* Integration test for the rest sql action. The one that speaks json directly to a
* user rather than to the JDBC driver or CLI.
*/
public class RestSqlIT extends RestSqlTestCase {
}

View File

@ -1,32 +1,20 @@
import org.elasticsearch.gradle.test.RunTask
apply plugin: 'elasticsearch.standalone-rest-test'
apply plugin: 'elasticsearch.rest-test'
dependencies {
testCompile project(path: ':x-pack-elasticsearch:plugin', configuration: 'runtime')
testCompile project(path: ':x-pack-elasticsearch:plugin', configuration: 'testArtifacts')
testCompile project(path: ':modules:reindex')
}
// NOCOMMIT we should try this on multiple nodes
integTestCluster {
plugin ':x-pack-elasticsearch:plugin'
setting 'xpack.ml.enabled', 'false'
setting 'xpack.monitoring.enabled', 'false'
// Enabled audit logging so we can test it
// Setup auditing so we can use it in some tests
setting 'xpack.security.audit.enabled', 'true'
setting 'xpack.security.audit.outputs', 'index'
setting 'xpack.security.audit.outputs', '[logfile, index]'
// Only log the events we need so we don't have as much to sort through
setting 'xpack.security.audit.index.events.include', '[access_denied, access_granted]'
// Try and speed up audit logging without overwelming it
setting 'xpack.security.audit.index.flush_interval', '250ms'
setting 'xpack.security.audit.index.settings.index.number_of_shards', '1'
setting 'xpack.security.audit.index.settings.index.refresh_interval', '250ms'
// Setup roles used by tests
extraConfigFile 'x-pack/roles.yml', 'roles.yml'
/* Setup the one admin user that we run the tests as.
* Tests use "run as" to get different users. */
setupCommand 'setupUser#test_admin',
'bin/x-pack/users', 'useradd', 'test_admin', '-p', 'x-pack-test-password', '-r', 'superuser'
// Override the wait condition to work properly with security
waitCondition = { node, ant ->
File tmpFile = new File(node.cwd, 'wait.success')
ant.get(src: "http://${node.httpUri()}/_cluster/health?wait_for_nodes=>=${numNodes}&wait_for_status=yellow",
@ -39,23 +27,23 @@ integTestCluster {
}
}
task run(type: RunTask) {
distribution = 'zip' // NOCOMMIT make double sure we want all the modules
plugin ':x-pack-elasticsearch:plugin'
setting 'xpack.ml.enabled', 'false'
setting 'xpack.monitoring.enabled', 'false'
run {
// Enabled audit logging so we can test it
setting 'xpack.security.audit.enabled', 'true'
setting 'xpack.security.audit.outputs', 'index'
setting 'xpack.security.audit.outputs', '[logfile, index]'
// Only log the events we need so we don't have as much to sort through
setting 'xpack.security.audit.index.events.include', '[access_denied, access_granted]'
// Try and speed up the logging process without overwelming it
setting 'xpack.security.audit.index.flush_interval', '250ms'
setting 'xpack.security.audit.index.settings.index.number_of_shards', '1'
setting 'xpack.security.audit.index.settings.index.refresh_interval', '250ms'
// Setup roles used by tests
extraConfigFile 'x-pack/roles.yml', 'roles.yml'
/* Setup the one admin user that we run the tests as.
* Tests use "run as" to get different users. */
setupCommand 'setupUser#test_admin',
'bin/x-pack/users', 'useradd', 'test_admin', '-p', 'x-pack-test-password', '-r', 'superuser'
// Override the wait condition to work properly with security
waitCondition = { node, ant ->
File tmpFile = new File(node.cwd, 'wait.success')
ant.get(src: "http://${node.httpUri()}/_cluster/health?wait_for_nodes=>=${numNodes}&wait_for_status=yellow",

View File

@ -0,0 +1,31 @@
/*
* 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.xpack.sql.security;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.xpack.sql.RestSqlTestCase;
import static org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
/**
* Integration test for the rest sql action. The one that speaks json directly to a
* user rather than to the JDBC driver or CLI.
*/
public class RestSqlIT extends RestSqlTestCase {
/**
* All tests run as a an administrative user but use
* <code>es-security-runas-user</code> to become a less privileged user when needed.
*/
@Override
protected Settings restClientSettings() {
String token = basicAuthHeaderValue("test_admin", new SecureString("x-pack-test-password".toCharArray()));
return Settings.builder()
.put(ThreadContext.PREFIX + ".Authorization", token)
.build();
}
}

View File

@ -3,12 +3,13 @@
* 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.xpack.sql;
package org.elasticsearch.xpack.sql.security;
import org.apache.http.Header;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicHeader;
import org.apache.logging.log4j.util.Strings;
import org.elasticsearch.client.Response;
import org.elasticsearch.client.ResponseException;
import org.elasticsearch.common.CheckedFunction;
@ -31,18 +32,21 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static java.util.Collections.unmodifiableMap;
import static org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
import static org.elasticsearch.xpack.sql.RestSqlTestCase.columnInfo;
import static org.hamcrest.Matchers.containsString;
public class SqlSecurityIT extends ESRestTestCase {
private static boolean createdTestData = false;
private static boolean oneTimeSetup = false;
private static boolean auditFailure = false;
/**
@ -66,8 +70,8 @@ public class SqlSecurityIT extends ESRestTestCase {
}
@Before
public void createTestData() throws IOException {
if (createdTestData) {
public void oneTimeSetup() throws Exception {
if (oneTimeSetup) {
/* Since we don't wipe the cluster between tests we only need to
* write the test data once. */
return;
@ -79,14 +83,43 @@ public class SqlSecurityIT extends ESRestTestCase {
bulk.append("{\"a\": 4, \"b\": 5, \"c\": 6}\n");
client().performRequest("PUT", "/test/test/_bulk", singletonMap("refresh", "true"),
new StringEntity(bulk.toString(), ContentType.APPLICATION_JSON));
/* Wait for the audit log to go quiet and then clear it to protect
* us from log events coming from other tests. */
cleanAuditLog();
oneTimeSetup = true;
}
/**
* Wait for any running bulk tasks to complete because those
* are likely audit log events and will cause the tests to
* hang at best and at worst. Then remove all audit logs.
*/
@After
public void clearAuditLog() throws Exception {
public void cleanAuditLog() throws Exception {
assertBusy(() -> {
Set<String> bulks = new HashSet<>();
Map<?, ?> nodes = (Map<?, ?>) entityAsMap(adminClient().performRequest("GET", "_tasks")).get("nodes");
for (Map.Entry<?, ?> node : nodes.entrySet()) {
Map<?, ?> nodeInfo = (Map<?, ?>) node.getValue();
Map<?, ?> nodeTasks = (Map<?, ?>) nodeInfo.get("tasks");
for (Map.Entry<?, ?> taskAndName : nodeTasks.entrySet()) {
Map<?, ?> task = (Map<?, ?>) taskAndName.getValue();
String action = task.get("action").toString();
if ("indices:data/write/bulk".equals(action) || "indices:data/write/bulk[s]".equals(action)) {
bulks.add(task.toString());
}
}
}
if (false == bulks.isEmpty()) {
String bulksString = Strings.join(bulks, '\n');
logger.info("Waiting on bulk writes to finish:\n{}", bulksString);
fail("Waiting on bulk writes to finish:\n" + bulksString);
}
}, 1, TimeUnit.MINUTES);
try {
clearAuditEvents();
} catch (ResponseException e) {
// 404 here just means we had no indexes
// 404 here just means we don't have any audit log index which shouldn't fail
if (e.getResponse().getStatusLine().getStatusCode() != 404) {
throw e;
}
@ -268,13 +301,15 @@ public class SqlSecurityIT extends ESRestTestCase {
search.endObject();
Map<String, Object> audit;
try {
audit = toMap(client().performRequest("POST", "/.security_audit_log-*/_search",
singletonMap("filter_path", "hits.hits._source"), new StringEntity(search.string(), ContentType.APPLICATION_JSON)));
audit = toMap(client().performRequest("POST", "/.security_audit_log-*/_search?size=1000",
emptyMap(), new StringEntity(search.string(), ContentType.APPLICATION_JSON)));
} catch (ResponseException e) {
throw new AssertionError("ES failed to respond. Wrapping in assertion so we retry. Hopefully this is transient.", e);
}
Map<?, ?> hitsOuter = (Map<?, ?>) audit.get("hits");
assertNotNull("expected some hits", hitsOuter);
if (hitsOuter == null) {
fail("expected some hit but got:\n" + audit);
}
List<?> hits = (List<?>) hitsOuter.get("hits");
verifier: for (CheckedFunction<Map<?, ?>, Boolean, Exception> eventChecker : eventCheckers) {
for (Object hit : hits) {
@ -283,11 +318,25 @@ public class SqlSecurityIT extends ESRestTestCase {
continue verifier;
}
}
fail("didn't find audit event we were looking for. found [" + hits + "]");
fail("didn't find audit event we were looking for. found " + hits);
}
});
}, 1, TimeUnit.MINUTES);
} catch (AssertionError e) {
auditFailure = true;
logger.warn("Failed to find an audit log. Skipping remaining tests in this class after this the missing audit"
+ "logs could turn up later.");
Map<String, Object> audit = toMap(
client().performRequest("POST", "/.security_audit_log-*/_search?size=50&sort=@timestamp:desc"));
Map<?, ?> hitsOuter = (Map<?, ?>) audit.get("hits");
List<?> hits = hitsOuter == null ? null : (List<?>) hitsOuter.get("hits");
if (hits == null || hits.isEmpty()) {
logger.warn("Didn't find any audit logs. Here is the whole response:\n{}", audit);
} else {
logger.warn("Here are the last 500 indexed audit logs:");
for (Object hit : hits) {
logger.warn(hit.toString());
}
}
throw e;
}
}
@ -299,6 +348,7 @@ public class SqlSecurityIT extends ESRestTestCase {
adminClient().performRequest("POST", "/.security_audit_log-*/_delete_by_query", emptyMap(),
new StringEntity("{\"query\":{\"match_all\":{}}}", ContentType.APPLICATION_JSON));
} catch (ResponseException e) {
logger.info("Conflict while clearing audit logs");
if (e.getResponse().getStatusLine().getStatusCode() == 409) {
throw new AssertionError("Conflict while clearing audit log. Wrapping in assertion so we retry.", e);
}
@ -310,12 +360,4 @@ public class SqlSecurityIT extends ESRestTestCase {
throw e;
}
}
private Map<String, Object> columnInfo(String name, String type) {
Map<String, Object> column = new HashMap<>();
column.put("name", name);
column.put("type", type);
return unmodifiableMap(column);
}
}

View File

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.sql;
import org.apache.http.HttpEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
@ -31,10 +30,20 @@ import static org.hamcrest.Matchers.both;
import static org.hamcrest.Matchers.containsString;
/**
* Integration test for the rest sql action. That one that speaks json directly to a
* Integration test for the rest sql action. The one that speaks json directly to a
* user rather than to the JDBC driver or CLI.
*/
public class RestSqlIT extends ESRestTestCase {
public abstract class RestSqlTestCase extends ESRestTestCase {
/**
* Builds that map that is returned in the header for each column.
*/
public static Map<String, Object> columnInfo(String name, String type) {
Map<String, Object> column = new HashMap<>();
column.put("name", name);
column.put("type", type);
return unmodifiableMap(column);
}
public void testBasicQuery() throws IOException {
StringBuilder bulk = new StringBuilder();
bulk.append("{\"index\":{\"_id\":\"1\"}}\n");
@ -165,11 +174,4 @@ public class RestSqlIT extends ESRestTestCase {
fail("Response does not match:\n" + message.toString());
}
}
private Map<String, Object> columnInfo(String name, String type) {
Map<String, Object> column = new HashMap<>();
column.put("name", name);
column.put("type", type);
return unmodifiableMap(column);
}
}