From 2d852b9cb392a077596b848f25e599e99b63590f Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Tue, 5 Jun 2018 08:44:43 +0200 Subject: [PATCH 1/2] Add QA project and fixture based test for discovery-ec2 plugin This commit adds a new QA sub project to the discovery-ec2 plugin. This project uses a fixture to test the plugin using a multi-node cluster. Once all nodes are started, the nodes transport addresses are written in a file that is later read by the fixture. --- plugins/discovery-ec2/build.gradle | 5 + .../discovery-ec2/qa/amazon-ec2/build.gradle | 78 ++++++ ...azonEC2DiscoveryClientYamlTestSuiteIT.java | 37 +++ .../discovery/ec2/AmazonEC2Fixture.java} | 245 ++++++++++-------- .../test/discovery_ec2/10_basic.yml | 9 + plugins/discovery-ec2/qa/build.gradle | 0 6 files changed, 266 insertions(+), 108 deletions(-) create mode 100644 plugins/discovery-ec2/qa/amazon-ec2/build.gradle create mode 100644 plugins/discovery-ec2/qa/amazon-ec2/src/test/java/org/elasticsearch/discovery/ec2/AmazonEC2DiscoveryClientYamlTestSuiteIT.java rename plugins/discovery-ec2/{src/test/java/org/elasticsearch/discovery/ec2/Ec2DiscoveryClusterFormationTests.java => qa/amazon-ec2/src/test/java/org/elasticsearch/discovery/ec2/AmazonEC2Fixture.java} (50%) create mode 100644 plugins/discovery-ec2/qa/amazon-ec2/src/test/resources/rest-api-spec/test/discovery_ec2/10_basic.yml create mode 100644 plugins/discovery-ec2/qa/build.gradle diff --git a/plugins/discovery-ec2/build.gradle b/plugins/discovery-ec2/build.gradle index 7daf944f81898..b1c3b62fd6edf 100644 --- a/plugins/discovery-ec2/build.gradle +++ b/plugins/discovery-ec2/build.gradle @@ -53,6 +53,11 @@ test { systemProperty 'tests.artifact', project.name } +check { + // also execute the QA tests when testing the plugin + dependsOn 'qa:amazon-ec2:check' +} + thirdPartyAudit.excludes = [ // classes are missing 'com.amazonaws.jmespath.JmesPathEvaluationVisitor', diff --git a/plugins/discovery-ec2/qa/amazon-ec2/build.gradle b/plugins/discovery-ec2/qa/amazon-ec2/build.gradle new file mode 100644 index 0000000000000..2e1328d172194 --- /dev/null +++ b/plugins/discovery-ec2/qa/amazon-ec2/build.gradle @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + + +import org.elasticsearch.gradle.MavenFilteringHack +import org.elasticsearch.gradle.test.AntFixture + +apply plugin: 'elasticsearch.standalone-rest-test' +apply plugin: 'elasticsearch.rest-test' + +dependencies { + testCompile project(path: ':plugins:discovery-ec2', configuration: 'runtime') +} + +forbiddenApisTest { + // we are using jdk-internal instead of jdk-non-portable to allow for com.sun.net.httpserver.* usage + bundledSignatures -= 'jdk-non-portable' + bundledSignatures += 'jdk-internal' +} + +final int ec2NumberOfNodes = 3 +File ec2DiscoveryFile = new File(project.buildDir, 'generated-resources/nodes.uri') + +/** A task to start the AmazonEC2Fixture which emulates an EC2 service **/ +task ec2Fixture(type: AntFixture) { + dependsOn compileTestJava + env 'CLASSPATH', "${ -> project.sourceSets.test.runtimeClasspath.asPath }" + executable = new File(project.runtimeJavaHome, 'bin/java') + args 'org.elasticsearch.discovery.ec2.AmazonEC2Fixture', baseDir, ec2DiscoveryFile.absolutePath +} + +Map expansions = [ + 'expected_nodes': ec2NumberOfNodes +] + +processTestResources { + inputs.properties(expansions) + MavenFilteringHack.filter(it, expansions) +} + +integTestCluster { + dependsOn ec2Fixture + numNodes = ec2NumberOfNodes + plugin ':plugins:discovery-ec2' + keystoreSetting 'discovery.ec2.access_key', 'ec2_integration_test_access_key' + keystoreSetting 'discovery.ec2.secret_key', 'ec2_integration_test_secret_key' + setting 'discovery.zen.hosts_provider', 'ec2' + setting 'discovery.ec2.endpoint', "http://${-> ec2Fixture.addressAndPort}" + unicastTransportUri = { seedNode, node, ant -> return null } + + waitCondition = { node, ant -> + ec2DiscoveryFile.parentFile.mkdirs() + ec2DiscoveryFile.setText(integTest.nodes.collect { n -> "${n.transportUri()}" }.join('\n'), 'UTF-8') + + File tmpFile = new File(node.cwd, 'wait.success') + ant.get(src: "http://${node.httpUri()}/_cluster/health?wait_for_nodes=>=${ec2NumberOfNodes}&wait_for_status=yellow", + dest: tmpFile.toString(), + ignoreerrors: true, + retries: 10) + return tmpFile.exists() + } +} diff --git a/plugins/discovery-ec2/qa/amazon-ec2/src/test/java/org/elasticsearch/discovery/ec2/AmazonEC2DiscoveryClientYamlTestSuiteIT.java b/plugins/discovery-ec2/qa/amazon-ec2/src/test/java/org/elasticsearch/discovery/ec2/AmazonEC2DiscoveryClientYamlTestSuiteIT.java new file mode 100644 index 0000000000000..09d5a8d6fdf28 --- /dev/null +++ b/plugins/discovery-ec2/qa/amazon-ec2/src/test/java/org/elasticsearch/discovery/ec2/AmazonEC2DiscoveryClientYamlTestSuiteIT.java @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.discovery.ec2; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; +import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate; +import org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase; + +public class AmazonEC2DiscoveryClientYamlTestSuiteIT extends ESClientYamlSuiteTestCase { + + public AmazonEC2DiscoveryClientYamlTestSuiteIT(@Name("yaml") ClientYamlTestCandidate testCandidate) { + super(testCandidate); + } + + @ParametersFactory + public static Iterable parameters() throws Exception { + return ESClientYamlSuiteTestCase.createParameters(); + } +} diff --git a/plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/Ec2DiscoveryClusterFormationTests.java b/plugins/discovery-ec2/qa/amazon-ec2/src/test/java/org/elasticsearch/discovery/ec2/AmazonEC2Fixture.java similarity index 50% rename from plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/Ec2DiscoveryClusterFormationTests.java rename to plugins/discovery-ec2/qa/amazon-ec2/src/test/java/org/elasticsearch/discovery/ec2/AmazonEC2Fixture.java index 49fd9de71ecfa..7382d45e84fe1 100644 --- a/plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/Ec2DiscoveryClusterFormationTests.java +++ b/plugins/discovery-ec2/qa/amazon-ec2/src/test/java/org/elasticsearch/discovery/ec2/AmazonEC2Fixture.java @@ -16,106 +16,166 @@ * specific language governing permissions and limitations * under the License. */ - package org.elasticsearch.discovery.ec2; import com.amazonaws.util.IOUtils; import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import org.apache.http.NameValuePair; import org.apache.http.client.utils.URLEncodedUtils; import org.elasticsearch.common.SuppressForbidden; -import org.elasticsearch.common.io.FileSystemUtils; -import org.elasticsearch.common.logging.Loggers; -import org.elasticsearch.common.settings.MockSecureSettings; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.discovery.DiscoveryModule; import org.elasticsearch.mocksocket.MockHttpServer; -import org.elasticsearch.plugins.Plugin; -import org.elasticsearch.test.ESIntegTestCase; -import org.junit.AfterClass; -import org.junit.BeforeClass; +import org.elasticsearch.rest.RestStatus; import javax.xml.XMLConstants; import javax.xml.stream.XMLOutputFactory; -import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; import java.io.IOException; -import java.io.OutputStream; import java.io.StringWriter; +import java.lang.management.ManagementFactory; +import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.util.List; import java.util.UUID; -import java.util.concurrent.ExecutionException; +import java.util.function.Predicate; -import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoTimeout; -import static org.hamcrest.Matchers.equalTo; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; -@ESIntegTestCase.ClusterScope(supportsDedicatedMasters = false, numDataNodes = 2, numClientNodes = 0) -@SuppressForbidden(reason = "use http server") -// TODO this should be a IT but currently all ITs in this project run against a real cluster -public class Ec2DiscoveryClusterFormationTests extends ESIntegTestCase { +/** + * {@link AmazonEC2Fixture} is a fixture that emulates an AWS EC2 service. + *

+ * It starts an asynchronous socket server that binds to a random local port. + */ +public class AmazonEC2Fixture { - private static HttpServer httpServer; - private static Path logDir; + public static void main(String[] args) throws Exception { + if (args == null || args.length != 2) { + throw new IllegalArgumentException("AmazonEC2Fixture "); + } - @Override - protected Collection> nodePlugins() { - return Arrays.asList(Ec2DiscoveryPlugin.class); - } + final InetSocketAddress socketAddress = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); + final HttpServer httpServer = MockHttpServer.createHttp(socketAddress, 0); - @Override - protected Settings nodeSettings(int nodeOrdinal) { - Path resolve = logDir.resolve(Integer.toString(nodeOrdinal)); try { - Files.createDirectory(resolve); - } catch (IOException e) { - throw new RuntimeException(e); + final Path workingDirectory = toPath(args[0]); + /// Writes the PID of the current Java process in a `pid` file located in the working directory + writeFile(workingDirectory, "pid", ManagementFactory.getRuntimeMXBean().getName().split("@")[0]); + + final String addressAndPort = addressToString(httpServer.getAddress()); + // Writes the address and port of the http server in a `ports` file located in the working directory + writeFile(workingDirectory, "ports", addressAndPort); + + httpServer.createContext("/", new ResponseHandler(toPath(args[1]))); + httpServer.start(); + + // Wait to be killed + Thread.sleep(Long.MAX_VALUE); + + } finally { + httpServer.stop(0); } - MockSecureSettings secureSettings = new MockSecureSettings(); - secureSettings.setString(AwsEc2Service.ACCESS_KEY_SETTING.getKey(), "some_access"); - secureSettings.setString(AwsEc2Service.SECRET_KEY_SETTING.getKey(), "some_secret"); - return Settings.builder().put(super.nodeSettings(nodeOrdinal)) - .put(DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.getKey(), "ec2") - .put("path.logs", resolve) - .put("transport.tcp.port", 0) - .put("node.portsfile", "true") - .put(AwsEc2Service.ENDPOINT_SETTING.getKey(), "http://" + httpServer.getAddress().getHostName() + ":" + - httpServer.getAddress().getPort()) - .setSecureSettings(secureSettings) - .build(); } - /** - * Creates mock EC2 endpoint providing the list of started nodes to the DescribeInstances API call - */ - @BeforeClass - public static void startHttpd() throws Exception { - logDir = createTempDir(); - httpServer = MockHttpServer.createHttp(new InetSocketAddress(InetAddress.getLoopbackAddress().getHostAddress(), 0), 0); - - httpServer.createContext("/", (s) -> { - Headers headers = s.getResponseHeaders(); - headers.add("Content-Type", "text/xml; charset=UTF-8"); - String action = null; - for (NameValuePair parse : URLEncodedUtils.parse(IOUtils.toString(s.getRequestBody()), StandardCharsets.UTF_8)) { - if ("Action".equals(parse.getName())) { - action = parse.getValue(); - break; + @SuppressForbidden(reason = "Paths#get is fine - we don't have environment here") + private static Path toPath(final String dir) { + return Paths.get(dir); + } + + private static void writeFile(final Path dir, final String fileName, final String content) throws IOException { + final Path tempPidFile = Files.createTempFile(dir, null, null); + Files.write(tempPidFile, singleton(content)); + Files.move(tempPidFile, dir.resolve(fileName), StandardCopyOption.ATOMIC_MOVE); + } + + private static String addressToString(final SocketAddress address) { + final InetSocketAddress inetSocketAddress = (InetSocketAddress) address; + if (inetSocketAddress.getAddress() instanceof Inet6Address) { + return "[" + inetSocketAddress.getHostString() + "]:" + inetSocketAddress.getPort(); + } else { + return inetSocketAddress.getHostString() + ":" + inetSocketAddress.getPort(); + } + } + + static class ResponseHandler implements HttpHandler { + + private final Path discoveryPath; + + ResponseHandler(final Path discoveryPath) { + this.discoveryPath = discoveryPath; + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + RestStatus responseStatus = RestStatus.INTERNAL_SERVER_ERROR; + String responseBody = null; + String responseContentType = "text/plain"; + + final String path = exchange.getRequestURI().getRawPath(); + if ("/".equals(path)) { + final String method = exchange.getRequestMethod(); + final Headers headers = exchange.getRequestHeaders(); + + if ("GET".equals(method) && matchingHeader(headers, "User-agent", v -> v.startsWith("Apache Ant"))) { + // Replies to the fixture's waiting condition + responseStatus = RestStatus.OK; + responseBody = "AmazonEC2Fixture"; + + } else if ("POST".equals(method) && matchingHeader(headers, "User-agent", v -> v.startsWith("aws-sdk-java"))) { + // Simulate an EC2 DescribeInstancesResponse + responseStatus = RestStatus.OK; + responseContentType = "text/xml; charset=UTF-8"; + + for (NameValuePair parse : URLEncodedUtils.parse(IOUtils.toString(exchange.getRequestBody()), StandardCharsets.UTF_8)) { + if ("Action".equals(parse.getName())) { + responseBody = generateDescribeInstancesResponse(); + break; + } + } + } + } + + final byte[] response = responseBody != null ? responseBody.getBytes(StandardCharsets.UTF_8) : new byte[0]; + exchange.sendResponseHeaders(responseStatus.getStatus(), response.length); + exchange.getResponseHeaders().put("Content-Type", singletonList(responseContentType)); + if (response.length > 0) { + exchange.getResponseBody().write(response); + } + exchange.close(); + } + + /** Checks if the given {@link Headers} contains a header with a given name which has a value that matches a predicate **/ + private boolean matchingHeader(final Headers headers, final String headerName, final Predicate predicate) { + if (headers != null && headers.isEmpty() == false) { + final List values = headers.get(headerName); + if (values != null) { + for (String value : values) { + if (predicate.test(value)) { + return true; + } + } } } - assertThat(action, equalTo("DescribeInstances")); + return false; + } - XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newFactory(); + /** + * Generates a XML response that describe the EC2 instances + */ + private String generateDescribeInstancesResponse() { + final XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newFactory(); xmlOutputFactory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true); - StringWriter out = new StringWriter(); + + final StringWriter out = new StringWriter(); XMLStreamWriter sw; try { sw = xmlOutputFactory.createXMLStreamWriter(out); @@ -131,12 +191,8 @@ public static void startHttpd() throws Exception { sw.writeStartElement("reservationSet"); { - Path[] files = FileSystemUtils.files(logDir); - for (int i = 0; i < files.length; i++) { - Path resolve = files[i].resolve("transport.ports"); - if (Files.exists(resolve)) { - List addresses = Files.readAllLines(resolve); - Collections.shuffle(addresses, random()); + if (Files.exists(discoveryPath)) { + for (String address : Files.readAllLines(discoveryPath)) { sw.writeStartElement("item"); { @@ -169,11 +225,11 @@ public static void startHttpd() throws Exception { sw.writeEndElement(); sw.writeStartElement("privateDnsName"); - sw.writeCharacters(addresses.get(0)); + sw.writeCharacters(address); sw.writeEndElement(); sw.writeStartElement("dnsName"); - sw.writeCharacters(addresses.get(0)); + sw.writeCharacters(address); sw.writeEndElement(); sw.writeStartElement("instanceType"); @@ -195,11 +251,11 @@ public static void startHttpd() throws Exception { sw.writeEndElement(); sw.writeStartElement("privateIpAddress"); - sw.writeCharacters(addresses.get(0)); + sw.writeCharacters(address); sw.writeEndElement(); sw.writeStartElement("ipAddress"); - sw.writeCharacters(addresses.get(0)); + sw.writeCharacters(address); sw.writeEndElement(); } sw.writeEndElement(); @@ -209,44 +265,17 @@ public static void startHttpd() throws Exception { sw.writeEndElement(); } } + sw.writeEndElement(); } sw.writeEndElement(); + + sw.writeEndDocument(); + sw.flush(); } - sw.writeEndElement(); - - sw.writeEndDocument(); - sw.flush(); - - final byte[] responseAsBytes = out.toString().getBytes(StandardCharsets.UTF_8); - s.sendResponseHeaders(200, responseAsBytes.length); - OutputStream responseBody = s.getResponseBody(); - responseBody.write(responseAsBytes); - responseBody.close(); - } catch (XMLStreamException e) { - Loggers.getLogger(Ec2DiscoveryClusterFormationTests.class).error("Failed serializing XML", e); + } catch (Exception e) { throw new RuntimeException(e); } - }); - - httpServer.start(); - } - - @AfterClass - public static void stopHttpd() throws IOException { - for (int i = 0; i < internalCluster().size(); i++) { - // shut them all down otherwise we get spammed with connection refused exceptions - internalCluster().stopRandomDataNode(); + return out.toString(); } - httpServer.stop(0); - httpServer = null; - logDir = null; - } - - public void testJoin() throws ExecutionException, InterruptedException { - // only wait for the cluster to form - assertNoTimeout(client().admin().cluster().prepareHealth().setWaitForNodes(Integer.toString(2)).get()); - // add one more node and wait for it to join - internalCluster().startDataOnlyNode(); - assertNoTimeout(client().admin().cluster().prepareHealth().setWaitForNodes(Integer.toString(3)).get()); } } diff --git a/plugins/discovery-ec2/qa/amazon-ec2/src/test/resources/rest-api-spec/test/discovery_ec2/10_basic.yml b/plugins/discovery-ec2/qa/amazon-ec2/src/test/resources/rest-api-spec/test/discovery_ec2/10_basic.yml new file mode 100644 index 0000000000000..f9d42eee088a3 --- /dev/null +++ b/plugins/discovery-ec2/qa/amazon-ec2/src/test/resources/rest-api-spec/test/discovery_ec2/10_basic.yml @@ -0,0 +1,9 @@ +# Integration tests for discovery-ec2 +--- +"All nodes are correctly discovered": + + - do: + nodes.info: + metric: [ transport ] + + - match: { _nodes.total: ${expected_nodes} } diff --git a/plugins/discovery-ec2/qa/build.gradle b/plugins/discovery-ec2/qa/build.gradle new file mode 100644 index 0000000000000..e69de29bb2d1d From a8c976ba8519d0c8585ecc272e4d1459bf75f9d5 Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Thu, 14 Jun 2018 15:00:35 +0200 Subject: [PATCH 2/2] Rebase and apply feedback --- .../discovery-ec2/qa/amazon-ec2/build.gradle | 8 +- .../discovery/ec2/AmazonEC2Fixture.java | 303 +++++++----------- .../test/discovery_ec2/10_basic.yml | 6 + 3 files changed, 115 insertions(+), 202 deletions(-) diff --git a/plugins/discovery-ec2/qa/amazon-ec2/build.gradle b/plugins/discovery-ec2/qa/amazon-ec2/build.gradle index 2e1328d172194..90fac9e80cd78 100644 --- a/plugins/discovery-ec2/qa/amazon-ec2/build.gradle +++ b/plugins/discovery-ec2/qa/amazon-ec2/build.gradle @@ -28,12 +28,6 @@ dependencies { testCompile project(path: ':plugins:discovery-ec2', configuration: 'runtime') } -forbiddenApisTest { - // we are using jdk-internal instead of jdk-non-portable to allow for com.sun.net.httpserver.* usage - bundledSignatures -= 'jdk-non-portable' - bundledSignatures += 'jdk-internal' -} - final int ec2NumberOfNodes = 3 File ec2DiscoveryFile = new File(project.buildDir, 'generated-resources/nodes.uri') @@ -69,7 +63,7 @@ integTestCluster { ec2DiscoveryFile.setText(integTest.nodes.collect { n -> "${n.transportUri()}" }.join('\n'), 'UTF-8') File tmpFile = new File(node.cwd, 'wait.success') - ant.get(src: "http://${node.httpUri()}/_cluster/health?wait_for_nodes=>=${ec2NumberOfNodes}&wait_for_status=yellow", + ant.get(src: "http://${node.httpUri()}/", dest: tmpFile.toString(), ignoreerrors: true, retries: 10) diff --git a/plugins/discovery-ec2/qa/amazon-ec2/src/test/java/org/elasticsearch/discovery/ec2/AmazonEC2Fixture.java b/plugins/discovery-ec2/qa/amazon-ec2/src/test/java/org/elasticsearch/discovery/ec2/AmazonEC2Fixture.java index 7382d45e84fe1..0cf4cbdeadb34 100644 --- a/plugins/discovery-ec2/qa/amazon-ec2/src/test/java/org/elasticsearch/discovery/ec2/AmazonEC2Fixture.java +++ b/plugins/discovery-ec2/qa/amazon-ec2/src/test/java/org/elasticsearch/discovery/ec2/AmazonEC2Fixture.java @@ -18,264 +18,177 @@ */ package org.elasticsearch.discovery.ec2; -import com.amazonaws.util.IOUtils; -import com.sun.net.httpserver.Headers; -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpHandler; -import com.sun.net.httpserver.HttpServer; import org.apache.http.NameValuePair; import org.apache.http.client.utils.URLEncodedUtils; import org.elasticsearch.common.SuppressForbidden; -import org.elasticsearch.mocksocket.MockHttpServer; import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.test.fixture.AbstractHttpFixture; import javax.xml.XMLConstants; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamWriter; import java.io.IOException; import java.io.StringWriter; -import java.lang.management.ManagementFactory; -import java.net.Inet6Address; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.util.List; +import java.util.Objects; import java.util.UUID; -import java.util.function.Predicate; -import static java.util.Collections.singleton; -import static java.util.Collections.singletonList; +import static java.nio.charset.StandardCharsets.UTF_8; /** * {@link AmazonEC2Fixture} is a fixture that emulates an AWS EC2 service. - *

- * It starts an asynchronous socket server that binds to a random local port. */ -public class AmazonEC2Fixture { +public class AmazonEC2Fixture extends AbstractHttpFixture { + + private final Path nodes; + + private AmazonEC2Fixture(final String workingDir, final String nodesUriPath) { + super(workingDir); + this.nodes = toPath(Objects.requireNonNull(nodesUriPath)); + } public static void main(String[] args) throws Exception { if (args == null || args.length != 2) { throw new IllegalArgumentException("AmazonEC2Fixture "); } - final InetSocketAddress socketAddress = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); - final HttpServer httpServer = MockHttpServer.createHttp(socketAddress, 0); - - try { - final Path workingDirectory = toPath(args[0]); - /// Writes the PID of the current Java process in a `pid` file located in the working directory - writeFile(workingDirectory, "pid", ManagementFactory.getRuntimeMXBean().getName().split("@")[0]); - - final String addressAndPort = addressToString(httpServer.getAddress()); - // Writes the address and port of the http server in a `ports` file located in the working directory - writeFile(workingDirectory, "ports", addressAndPort); - - httpServer.createContext("/", new ResponseHandler(toPath(args[1]))); - httpServer.start(); - - // Wait to be killed - Thread.sleep(Long.MAX_VALUE); - - } finally { - httpServer.stop(0); - } - } - - @SuppressForbidden(reason = "Paths#get is fine - we don't have environment here") - private static Path toPath(final String dir) { - return Paths.get(dir); - } - - private static void writeFile(final Path dir, final String fileName, final String content) throws IOException { - final Path tempPidFile = Files.createTempFile(dir, null, null); - Files.write(tempPidFile, singleton(content)); - Files.move(tempPidFile, dir.resolve(fileName), StandardCopyOption.ATOMIC_MOVE); - } - - private static String addressToString(final SocketAddress address) { - final InetSocketAddress inetSocketAddress = (InetSocketAddress) address; - if (inetSocketAddress.getAddress() instanceof Inet6Address) { - return "[" + inetSocketAddress.getHostString() + "]:" + inetSocketAddress.getPort(); - } else { - return inetSocketAddress.getHostString() + ":" + inetSocketAddress.getPort(); - } + final AmazonEC2Fixture fixture = new AmazonEC2Fixture(args[0], args[1]); + fixture.listen(); } - static class ResponseHandler implements HttpHandler { - - private final Path discoveryPath; - - ResponseHandler(final Path discoveryPath) { - this.discoveryPath = discoveryPath; - } - - @Override - public void handle(HttpExchange exchange) throws IOException { - RestStatus responseStatus = RestStatus.INTERNAL_SERVER_ERROR; - String responseBody = null; - String responseContentType = "text/plain"; - - final String path = exchange.getRequestURI().getRawPath(); - if ("/".equals(path)) { - final String method = exchange.getRequestMethod(); - final Headers headers = exchange.getRequestHeaders(); - - if ("GET".equals(method) && matchingHeader(headers, "User-agent", v -> v.startsWith("Apache Ant"))) { - // Replies to the fixture's waiting condition - responseStatus = RestStatus.OK; - responseBody = "AmazonEC2Fixture"; - - } else if ("POST".equals(method) && matchingHeader(headers, "User-agent", v -> v.startsWith("aws-sdk-java"))) { - // Simulate an EC2 DescribeInstancesResponse - responseStatus = RestStatus.OK; - responseContentType = "text/xml; charset=UTF-8"; - - for (NameValuePair parse : URLEncodedUtils.parse(IOUtils.toString(exchange.getRequestBody()), StandardCharsets.UTF_8)) { - if ("Action".equals(parse.getName())) { - responseBody = generateDescribeInstancesResponse(); - break; - } - } - } - } - - final byte[] response = responseBody != null ? responseBody.getBytes(StandardCharsets.UTF_8) : new byte[0]; - exchange.sendResponseHeaders(responseStatus.getStatus(), response.length); - exchange.getResponseHeaders().put("Content-Type", singletonList(responseContentType)); - if (response.length > 0) { - exchange.getResponseBody().write(response); - } - exchange.close(); - } - - /** Checks if the given {@link Headers} contains a header with a given name which has a value that matches a predicate **/ - private boolean matchingHeader(final Headers headers, final String headerName, final Predicate predicate) { - if (headers != null && headers.isEmpty() == false) { - final List values = headers.get(headerName); - if (values != null) { - for (String value : values) { - if (predicate.test(value)) { - return true; - } + @Override + protected Response handle(final Request request) throws IOException { + if ("/".equals(request.getPath()) && ("POST".equals(request.getMethod()))) { + final String userAgent = request.getHeader("User-Agent"); + if (userAgent != null && userAgent.startsWith("aws-sdk-java")) { + // Simulate an EC2 DescribeInstancesResponse + byte[] responseBody = EMPTY_BYTE; + for (NameValuePair parse : URLEncodedUtils.parse(new String(request.getBody(), UTF_8), UTF_8)) { + if ("Action".equals(parse.getName())) { + responseBody = generateDescribeInstancesResponse(); + break; } } + return new Response(RestStatus.OK.getStatus(), contentType("text/xml; charset=UTF-8"), responseBody); } - return false; } + return null; + } - /** - * Generates a XML response that describe the EC2 instances - */ - private String generateDescribeInstancesResponse() { - final XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newFactory(); - xmlOutputFactory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true); - - final StringWriter out = new StringWriter(); - XMLStreamWriter sw; - try { - sw = xmlOutputFactory.createXMLStreamWriter(out); - sw.writeStartDocument(); + /** + * Generates a XML response that describe the EC2 instances + */ + private byte[] generateDescribeInstancesResponse() { + final XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newFactory(); + xmlOutputFactory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true); - String namespace = "http://ec2.amazonaws.com/doc/2013-02-01/"; - sw.setDefaultNamespace(namespace); - sw.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX, "DescribeInstancesResponse", namespace); + final StringWriter out = new StringWriter(); + XMLStreamWriter sw; + try { + sw = xmlOutputFactory.createXMLStreamWriter(out); + sw.writeStartDocument(); + + String namespace = "http://ec2.amazonaws.com/doc/2013-02-01/"; + sw.setDefaultNamespace(namespace); + sw.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX, "DescribeInstancesResponse", namespace); + { + sw.writeStartElement("requestId"); + sw.writeCharacters(UUID.randomUUID().toString()); + sw.writeEndElement(); + + sw.writeStartElement("reservationSet"); { - sw.writeStartElement("requestId"); - sw.writeCharacters(UUID.randomUUID().toString()); - sw.writeEndElement(); + if (Files.exists(nodes)) { + for (String address : Files.readAllLines(nodes)) { - sw.writeStartElement("reservationSet"); - { - if (Files.exists(discoveryPath)) { - for (String address : Files.readAllLines(discoveryPath)) { + sw.writeStartElement("item"); + { + sw.writeStartElement("reservationId"); + sw.writeCharacters(UUID.randomUUID().toString()); + sw.writeEndElement(); - sw.writeStartElement("item"); + sw.writeStartElement("instancesSet"); { - sw.writeStartElement("reservationId"); - sw.writeCharacters(UUID.randomUUID().toString()); - sw.writeEndElement(); - - sw.writeStartElement("instancesSet"); + sw.writeStartElement("item"); { - sw.writeStartElement("item"); - { - sw.writeStartElement("instanceId"); - sw.writeCharacters(UUID.randomUUID().toString()); - sw.writeEndElement(); - - sw.writeStartElement("imageId"); - sw.writeCharacters(UUID.randomUUID().toString()); - sw.writeEndElement(); - - sw.writeStartElement("instanceState"); - { - sw.writeStartElement("code"); - sw.writeCharacters("16"); - sw.writeEndElement(); + sw.writeStartElement("instanceId"); + sw.writeCharacters(UUID.randomUUID().toString()); + sw.writeEndElement(); - sw.writeStartElement("name"); - sw.writeCharacters("running"); - sw.writeEndElement(); - } - sw.writeEndElement(); + sw.writeStartElement("imageId"); + sw.writeCharacters(UUID.randomUUID().toString()); + sw.writeEndElement(); - sw.writeStartElement("privateDnsName"); - sw.writeCharacters(address); + sw.writeStartElement("instanceState"); + { + sw.writeStartElement("code"); + sw.writeCharacters("16"); sw.writeEndElement(); - sw.writeStartElement("dnsName"); - sw.writeCharacters(address); + sw.writeStartElement("name"); + sw.writeCharacters("running"); sw.writeEndElement(); + } + sw.writeEndElement(); - sw.writeStartElement("instanceType"); - sw.writeCharacters("m1.medium"); - sw.writeEndElement(); + sw.writeStartElement("privateDnsName"); + sw.writeCharacters(address); + sw.writeEndElement(); - sw.writeStartElement("placement"); - { - sw.writeStartElement("availabilityZone"); - sw.writeCharacters("use-east-1e"); - sw.writeEndElement(); + sw.writeStartElement("dnsName"); + sw.writeCharacters(address); + sw.writeEndElement(); - sw.writeEmptyElement("groupName"); + sw.writeStartElement("instanceType"); + sw.writeCharacters("m1.medium"); + sw.writeEndElement(); - sw.writeStartElement("tenancy"); - sw.writeCharacters("default"); - sw.writeEndElement(); - } + sw.writeStartElement("placement"); + { + sw.writeStartElement("availabilityZone"); + sw.writeCharacters("use-east-1e"); sw.writeEndElement(); - sw.writeStartElement("privateIpAddress"); - sw.writeCharacters(address); - sw.writeEndElement(); + sw.writeEmptyElement("groupName"); - sw.writeStartElement("ipAddress"); - sw.writeCharacters(address); + sw.writeStartElement("tenancy"); + sw.writeCharacters("default"); sw.writeEndElement(); } sw.writeEndElement(); + + sw.writeStartElement("privateIpAddress"); + sw.writeCharacters(address); + sw.writeEndElement(); + + sw.writeStartElement("ipAddress"); + sw.writeCharacters(address); + sw.writeEndElement(); } sw.writeEndElement(); } sw.writeEndElement(); } + sw.writeEndElement(); } - sw.writeEndElement(); } sw.writeEndElement(); - - sw.writeEndDocument(); - sw.flush(); } - } catch (Exception e) { - throw new RuntimeException(e); + sw.writeEndElement(); + + sw.writeEndDocument(); + sw.flush(); } - return out.toString(); + } catch (Exception e) { + throw new RuntimeException(e); } + return out.toString().getBytes(UTF_8); + } + + @SuppressForbidden(reason = "Paths#get is fine - we don't have environment here") + private static Path toPath(final String dir) { + return Paths.get(dir); } } diff --git a/plugins/discovery-ec2/qa/amazon-ec2/src/test/resources/rest-api-spec/test/discovery_ec2/10_basic.yml b/plugins/discovery-ec2/qa/amazon-ec2/src/test/resources/rest-api-spec/test/discovery_ec2/10_basic.yml index f9d42eee088a3..682327b72dd9e 100644 --- a/plugins/discovery-ec2/qa/amazon-ec2/src/test/resources/rest-api-spec/test/discovery_ec2/10_basic.yml +++ b/plugins/discovery-ec2/qa/amazon-ec2/src/test/resources/rest-api-spec/test/discovery_ec2/10_basic.yml @@ -1,4 +1,10 @@ # Integration tests for discovery-ec2 +setup: + - do: + cluster.health: + wait_for_status: green + wait_for_nodes: ${expected_nodes} + --- "All nodes are correctly discovered":