Skip to content

Commit 989e465

Browse files
authored
Use fixture to test repository-s3 plugin (#29296)
This commit adds a new fixture that emulates a S3 service in order to improve the existing integration tests. This is very similar to what has been made for Google Cloud Storage in #28788, and such tests would have helped a lot to catch bugs like #22534. The AmazonS3Fixture is brittle and only implements the very necessary stuff for the S3 repository to work, but at least it works and can be adapted for specific tests needs.
1 parent 2b07f63 commit 989e465

File tree

8 files changed

+906
-40
lines changed

8 files changed

+906
-40
lines changed

plugins/repository-gcs/build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ thirdPartyAudit.excludes = [
5858
'org.apache.log.Logger',
5959
]
6060

61+
forbiddenApisTest {
62+
// we are using jdk-internal instead of jdk-non-portable to allow for com.sun.net.httpserver.* usage
63+
bundledSignatures -= 'jdk-non-portable'
64+
bundledSignatures += 'jdk-internal'
65+
}
66+
6167
/** A task to start the GoogleCloudStorageFixture which emulates a Google Cloud Storage service **/
6268
task googleCloudStorageFixture(type: AntFixture) {
6369
dependsOn compileTestJava

plugins/repository-gcs/src/test/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageFixture.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,16 @@
5252
*/
5353
public class GoogleCloudStorageFixture {
5454

55-
@SuppressForbidden(reason = "PathUtils#get is fine - we don't have environment here")
5655
public static void main(String[] args) throws Exception {
5756
if (args == null || args.length != 2) {
5857
throw new IllegalArgumentException("GoogleCloudStorageFixture <working directory> <bucket>");
5958
}
6059

61-
final InetSocketAddress socketAddress = new InetSocketAddress(InetAddress.getLoopbackAddress(), 43635);
60+
final InetSocketAddress socketAddress = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
6261
final HttpServer httpServer = MockHttpServer.createHttp(socketAddress, 0);
6362

6463
try {
65-
final Path workingDirectory = Paths.get(args[0]);
64+
final Path workingDirectory = workingDir(args[0]);
6665
/// Writes the PID of the current Java process in a `pid` file located in the working directory
6766
writeFile(workingDirectory, "pid", ManagementFactory.getRuntimeMXBean().getName().split("@")[0]);
6867

@@ -86,6 +85,11 @@ public static void main(String[] args) throws Exception {
8685
}
8786
}
8887

88+
@SuppressForbidden(reason = "Paths#get is fine - we don't have environment here")
89+
private static Path workingDir(final String dir) {
90+
return Paths.get(dir);
91+
}
92+
8993
private static void writeFile(final Path dir, final String fileName, final String content) throws IOException {
9094
final Path tempPidFile = Files.createTempFile(dir, null, null);
9195
Files.write(tempPidFile, singleton(content));
@@ -101,7 +105,6 @@ private static String addressToString(final SocketAddress address) {
101105
}
102106
}
103107

104-
@SuppressForbidden(reason = "Use a http server")
105108
static class ResponseHandler implements HttpHandler {
106109

107110
private final GoogleCloudStorageTestServer storageServer;

plugins/repository-gcs/src/test/resources/rest-api-spec/test/repository_gcs/10_basic.yml

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@
1313
- match: { nodes.$master.plugins.0.name: repository-gcs }
1414
---
1515
"Snapshot/Restore with repository-gcs":
16-
- skip:
17-
version: " - 6.3.0"
18-
reason: repository-gcs was not testable through YAML tests until 6.3.0
1916

2017
# Register repository
2118
- do:
@@ -28,7 +25,15 @@
2825
client: "integration_test"
2926

3027
- match: { acknowledged: true }
31-
28+
29+
# Get repository
30+
- do:
31+
snapshot.get_repository:
32+
repository: repository
33+
34+
- match: {repository.settings.bucket : "bucket_test"}
35+
- match: {repository.settings.client : "integration_test"}
36+
3237
# Index documents
3338
- do:
3439
bulk:
@@ -180,7 +185,3 @@
180185
- do:
181186
snapshot.delete_repository:
182187
repository: repository
183-
184-
185-
186-

plugins/repository-s3/build.gradle

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import org.elasticsearch.gradle.test.AntFixture
2+
13
/*
24
* Licensed to Elasticsearch under one or more contributor
35
* license agreements. See the NOTICE file distributed with
@@ -64,9 +66,28 @@ test {
6466
exclude '**/*CredentialsTests.class'
6567
}
6668

69+
forbiddenApisTest {
70+
// we are using jdk-internal instead of jdk-non-portable to allow for com.sun.net.httpserver.* usage
71+
bundledSignatures -= 'jdk-non-portable'
72+
bundledSignatures += 'jdk-internal'
73+
}
74+
75+
/** A task to start the AmazonS3Fixture which emulates a S3 service **/
76+
task s3Fixture(type: AntFixture) {
77+
dependsOn compileTestJava
78+
env 'CLASSPATH', "${ -> project.sourceSets.test.runtimeClasspath.asPath }"
79+
executable = new File(project.runtimeJavaHome, 'bin/java')
80+
args 'org.elasticsearch.repositories.s3.AmazonS3Fixture', baseDir, 'bucket_test'
81+
}
82+
6783
integTestCluster {
68-
keystoreSetting 's3.client.default.access_key', 'myaccesskey'
69-
keystoreSetting 's3.client.default.secret_key', 'mysecretkey'
84+
dependsOn s3Fixture
85+
86+
keystoreSetting 's3.client.integration_test.access_key', "s3_integration_test_access_key"
87+
keystoreSetting 's3.client.integration_test.secret_key', "s3_integration_test_secret_key"
88+
89+
/* Use a closure on the string to delay evaluation until tests are executed */
90+
setting 's3.client.integration_test.endpoint', "http://${ -> s3Fixture.addressAndPort }"
7091
}
7192

7293
thirdPartyAudit.excludes = [
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.elasticsearch.repositories.s3;
20+
21+
import com.sun.net.httpserver.HttpExchange;
22+
import com.sun.net.httpserver.HttpHandler;
23+
import com.sun.net.httpserver.HttpServer;
24+
import org.elasticsearch.common.SuppressForbidden;
25+
import org.elasticsearch.common.io.Streams;
26+
import org.elasticsearch.mocksocket.MockHttpServer;
27+
import org.elasticsearch.repositories.s3.AmazonS3TestServer.Response;
28+
29+
import java.io.ByteArrayOutputStream;
30+
import java.io.IOException;
31+
import java.lang.management.ManagementFactory;
32+
import java.net.Inet6Address;
33+
import java.net.InetAddress;
34+
import java.net.InetSocketAddress;
35+
import java.net.SocketAddress;
36+
import java.nio.file.Files;
37+
import java.nio.file.Path;
38+
import java.nio.file.Paths;
39+
import java.nio.file.StandardCopyOption;
40+
import java.util.List;
41+
import java.util.Map;
42+
43+
import static java.util.Collections.singleton;
44+
import static java.util.Collections.singletonList;
45+
46+
/**
47+
* {@link AmazonS3Fixture} is a fixture that emulates a S3 service.
48+
* <p>
49+
* It starts an asynchronous socket server that binds to a random local port. The server parses
50+
* HTTP requests and uses a {@link AmazonS3TestServer} to handle them before returning
51+
* them to the client as HTTP responses.
52+
*/
53+
public class AmazonS3Fixture {
54+
55+
public static void main(String[] args) throws Exception {
56+
if (args == null || args.length != 2) {
57+
throw new IllegalArgumentException("AmazonS3Fixture <working directory> <bucket>");
58+
}
59+
60+
final InetSocketAddress socketAddress = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
61+
final HttpServer httpServer = MockHttpServer.createHttp(socketAddress, 0);
62+
63+
try {
64+
final Path workingDirectory = workingDir(args[0]);
65+
/// Writes the PID of the current Java process in a `pid` file located in the working directory
66+
writeFile(workingDirectory, "pid", ManagementFactory.getRuntimeMXBean().getName().split("@")[0]);
67+
68+
final String addressAndPort = addressToString(httpServer.getAddress());
69+
// Writes the address and port of the http server in a `ports` file located in the working directory
70+
writeFile(workingDirectory, "ports", addressAndPort);
71+
72+
// Emulates S3
73+
final String storageUrl = "http://" + addressAndPort;
74+
final AmazonS3TestServer storageTestServer = new AmazonS3TestServer(storageUrl);
75+
storageTestServer.createBucket(args[1]);
76+
77+
httpServer.createContext("/", new ResponseHandler(storageTestServer));
78+
httpServer.start();
79+
80+
// Wait to be killed
81+
Thread.sleep(Long.MAX_VALUE);
82+
83+
} finally {
84+
httpServer.stop(0);
85+
}
86+
}
87+
88+
@SuppressForbidden(reason = "Paths#get is fine - we don't have environment here")
89+
private static Path workingDir(final String dir) {
90+
return Paths.get(dir);
91+
}
92+
93+
private static void writeFile(final Path dir, final String fileName, final String content) throws IOException {
94+
final Path tempPidFile = Files.createTempFile(dir, null, null);
95+
Files.write(tempPidFile, singleton(content));
96+
Files.move(tempPidFile, dir.resolve(fileName), StandardCopyOption.ATOMIC_MOVE);
97+
}
98+
99+
private static String addressToString(final SocketAddress address) {
100+
final InetSocketAddress inetSocketAddress = (InetSocketAddress) address;
101+
if (inetSocketAddress.getAddress() instanceof Inet6Address) {
102+
return "[" + inetSocketAddress.getHostString() + "]:" + inetSocketAddress.getPort();
103+
} else {
104+
return inetSocketAddress.getHostString() + ":" + inetSocketAddress.getPort();
105+
}
106+
}
107+
108+
static class ResponseHandler implements HttpHandler {
109+
110+
private final AmazonS3TestServer storageServer;
111+
112+
private ResponseHandler(final AmazonS3TestServer storageServer) {
113+
this.storageServer = storageServer;
114+
}
115+
116+
@Override
117+
public void handle(HttpExchange exchange) throws IOException {
118+
String method = exchange.getRequestMethod();
119+
String path = storageServer.getEndpoint() + exchange.getRequestURI().getRawPath();
120+
String query = exchange.getRequestURI().getRawQuery();
121+
Map<String, List<String>> headers = exchange.getRequestHeaders();
122+
ByteArrayOutputStream out = new ByteArrayOutputStream();
123+
Streams.copy(exchange.getRequestBody(), out);
124+
125+
final Response storageResponse = storageServer.handle(method, path, query, headers, out.toByteArray());
126+
127+
Map<String, List<String>> responseHeaders = exchange.getResponseHeaders();
128+
responseHeaders.put("Content-Type", singletonList(storageResponse.contentType));
129+
storageResponse.headers.forEach((k, v) -> responseHeaders.put(k, singletonList(v)));
130+
exchange.sendResponseHeaders(storageResponse.status.getStatus(), storageResponse.body.length);
131+
if (storageResponse.body.length > 0) {
132+
exchange.getResponseBody().write(storageResponse.body);
133+
}
134+
exchange.close();
135+
}
136+
}
137+
}

0 commit comments

Comments
 (0)