Skip to content

Commit b0c4976

Browse files
authored
Extract IMDS test fixture from S3 fixture (#117324)
The S3 and IMDS services are separate things in practice, we shouldn't be conflating them as we do today. This commit introduces a new independent test fixture just for the IMDS endpoint and migrates the relevant tests to use it. Relates ES-9984
1 parent 2f8bb0b commit b0c4976

File tree

11 files changed

+433
-156
lines changed

11 files changed

+433
-156
lines changed

modules/repository-s3/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ dependencies {
4545
testImplementation project(':test:fixtures:s3-fixture')
4646
yamlRestTestImplementation project(":test:framework")
4747
yamlRestTestImplementation project(':test:fixtures:s3-fixture')
48+
yamlRestTestImplementation project(':test:fixtures:ec2-imds-fixture')
4849
yamlRestTestImplementation project(':test:fixtures:minio-fixture')
4950
internalClusterTestImplementation project(':test:fixtures:minio-fixture')
5051

modules/repository-s3/src/yamlRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3ClientYamlTestSuiteIT.java

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,47 +9,71 @@
99

1010
package org.elasticsearch.repositories.s3;
1111

12+
import fixture.aws.imds.Ec2ImdsHttpFixture;
1213
import fixture.s3.S3HttpFixture;
13-
import fixture.s3.S3HttpFixtureWithEC2;
1414
import fixture.s3.S3HttpFixtureWithSessionToken;
1515

1616
import com.carrotsearch.randomizedtesting.annotations.Name;
1717
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
1818
import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters;
1919
import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope;
2020

21+
import org.elasticsearch.cluster.routing.Murmur3HashFunction;
2122
import org.elasticsearch.test.cluster.ElasticsearchCluster;
2223
import org.elasticsearch.test.fixtures.testcontainers.TestContainersThreadFilter;
2324
import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate;
2425
import org.junit.ClassRule;
2526
import org.junit.rules.RuleChain;
2627
import org.junit.rules.TestRule;
2728

29+
import java.util.Set;
30+
2831
@ThreadLeakFilters(filters = { TestContainersThreadFilter.class })
2932
@ThreadLeakScope(ThreadLeakScope.Scope.NONE) // https://github.com/elastic/elasticsearch/issues/102482
3033
public class RepositoryS3ClientYamlTestSuiteIT extends AbstractRepositoryS3ClientYamlTestSuiteIT {
3134

32-
public static final S3HttpFixture s3Fixture = new S3HttpFixture();
33-
public static final S3HttpFixtureWithSessionToken s3HttpFixtureWithSessionToken = new S3HttpFixtureWithSessionToken();
34-
public static final S3HttpFixtureWithEC2 s3Ec2 = new S3HttpFixtureWithEC2();
35+
private static final String HASHED_SEED = Integer.toString(Murmur3HashFunction.hash(System.getProperty("tests.seed")));
36+
private static final String TEMPORARY_SESSION_TOKEN = "session_token-" + HASHED_SEED;
37+
private static final String IMDS_ACCESS_KEY = "imds-access-key-" + HASHED_SEED;
38+
private static final String IMDS_SESSION_TOKEN = "imds-session-token-" + HASHED_SEED;
39+
40+
private static final S3HttpFixture s3Fixture = new S3HttpFixture();
41+
42+
private static final S3HttpFixtureWithSessionToken s3HttpFixtureWithSessionToken = new S3HttpFixtureWithSessionToken(
43+
"session_token_bucket",
44+
"session_token_base_path_integration_tests",
45+
System.getProperty("s3TemporaryAccessKey"),
46+
TEMPORARY_SESSION_TOKEN
47+
);
48+
49+
private static final S3HttpFixtureWithSessionToken s3HttpFixtureWithImdsSessionToken = new S3HttpFixtureWithSessionToken(
50+
"ec2_bucket",
51+
"ec2_base_path",
52+
IMDS_ACCESS_KEY,
53+
IMDS_SESSION_TOKEN
54+
);
3555

36-
private static final String s3TemporarySessionToken = "session_token";
56+
private static final Ec2ImdsHttpFixture ec2ImdsHttpFixture = new Ec2ImdsHttpFixture(IMDS_ACCESS_KEY, IMDS_SESSION_TOKEN, Set.of());
3757

3858
public static ElasticsearchCluster cluster = ElasticsearchCluster.local()
3959
.module("repository-s3")
4060
.keystore("s3.client.integration_test_permanent.access_key", System.getProperty("s3PermanentAccessKey"))
4161
.keystore("s3.client.integration_test_permanent.secret_key", System.getProperty("s3PermanentSecretKey"))
4262
.keystore("s3.client.integration_test_temporary.access_key", System.getProperty("s3TemporaryAccessKey"))
4363
.keystore("s3.client.integration_test_temporary.secret_key", System.getProperty("s3TemporarySecretKey"))
44-
.keystore("s3.client.integration_test_temporary.session_token", s3TemporarySessionToken)
64+
.keystore("s3.client.integration_test_temporary.session_token", TEMPORARY_SESSION_TOKEN)
4565
.setting("s3.client.integration_test_permanent.endpoint", s3Fixture::getAddress)
4666
.setting("s3.client.integration_test_temporary.endpoint", s3HttpFixtureWithSessionToken::getAddress)
47-
.setting("s3.client.integration_test_ec2.endpoint", s3Ec2::getAddress)
48-
.systemProperty("com.amazonaws.sdk.ec2MetadataServiceEndpointOverride", s3Ec2::getAddress)
67+
.setting("s3.client.integration_test_ec2.endpoint", s3HttpFixtureWithImdsSessionToken::getAddress)
68+
.systemProperty("com.amazonaws.sdk.ec2MetadataServiceEndpointOverride", ec2ImdsHttpFixture::getAddress)
4969
.build();
5070

5171
@ClassRule
52-
public static TestRule ruleChain = RuleChain.outerRule(s3Fixture).around(s3Ec2).around(s3HttpFixtureWithSessionToken).around(cluster);
72+
public static TestRule ruleChain = RuleChain.outerRule(s3Fixture)
73+
.around(s3HttpFixtureWithSessionToken)
74+
.around(s3HttpFixtureWithImdsSessionToken)
75+
.around(ec2ImdsHttpFixture)
76+
.around(cluster);
5377

5478
@ParametersFactory
5579
public static Iterable<Object[]> parameters() throws Exception {

modules/repository-s3/src/yamlRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3EcsClientYamlTestSuiteIT.java

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,48 @@
99

1010
package org.elasticsearch.repositories.s3;
1111

12-
import fixture.s3.S3HttpFixtureWithECS;
12+
import fixture.aws.imds.Ec2ImdsHttpFixture;
13+
import fixture.s3.S3HttpFixtureWithSessionToken;
1314

1415
import com.carrotsearch.randomizedtesting.annotations.Name;
1516
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
1617

18+
import org.elasticsearch.cluster.routing.Murmur3HashFunction;
1719
import org.elasticsearch.test.cluster.ElasticsearchCluster;
1820
import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate;
1921
import org.junit.ClassRule;
2022
import org.junit.rules.RuleChain;
2123
import org.junit.rules.TestRule;
2224

25+
import java.util.Set;
26+
2327
public class RepositoryS3EcsClientYamlTestSuiteIT extends AbstractRepositoryS3ClientYamlTestSuiteIT {
24-
private static final S3HttpFixtureWithECS s3Ecs = new S3HttpFixtureWithECS();
28+
29+
private static final String HASHED_SEED = Integer.toString(Murmur3HashFunction.hash(System.getProperty("tests.seed")));
30+
private static final String ECS_ACCESS_KEY = "ecs-access-key-" + HASHED_SEED;
31+
private static final String ECS_SESSION_TOKEN = "ecs-session-token-" + HASHED_SEED;
32+
33+
private static final S3HttpFixtureWithSessionToken s3Fixture = new S3HttpFixtureWithSessionToken(
34+
"ecs_bucket",
35+
"ecs_base_path",
36+
ECS_ACCESS_KEY,
37+
ECS_SESSION_TOKEN
38+
);
39+
40+
private static final Ec2ImdsHttpFixture ec2ImdsHttpFixture = new Ec2ImdsHttpFixture(
41+
ECS_ACCESS_KEY,
42+
ECS_SESSION_TOKEN,
43+
Set.of("/ecs_credentials_endpoint")
44+
);
2545

2646
public static ElasticsearchCluster cluster = ElasticsearchCluster.local()
2747
.module("repository-s3")
28-
.setting("s3.client.integration_test_ecs.endpoint", s3Ecs::getAddress)
29-
.environment("AWS_CONTAINER_CREDENTIALS_FULL_URI", () -> (s3Ecs.getAddress() + "/ecs_credentials_endpoint"))
48+
.setting("s3.client.integration_test_ecs.endpoint", s3Fixture::getAddress)
49+
.environment("AWS_CONTAINER_CREDENTIALS_FULL_URI", () -> ec2ImdsHttpFixture.getAddress() + "/ecs_credentials_endpoint")
3050
.build();
3151

3252
@ClassRule
33-
public static TestRule ruleChain = RuleChain.outerRule(s3Ecs).around(cluster);
53+
public static TestRule ruleChain = RuleChain.outerRule(s3Fixture).around(ec2ImdsHttpFixture).around(cluster);
3454

3555
@ParametersFactory
3656
public static Iterable<Object[]> parameters() throws Exception {

settings.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ List projects = [
8787
'server',
8888
'test:framework',
8989
'test:fixtures:azure-fixture',
90+
'test:fixtures:ec2-imds-fixture',
9091
'test:fixtures:gcs-fixture',
9192
'test:fixtures:hdfs-fixture',
9293
'test:fixtures:krb5kdc-fixture',
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
apply plugin: 'elasticsearch.java'
10+
11+
description = 'Fixture for emulating the Instance Metadata Service (IMDS) running in AWS EC2'
12+
13+
dependencies {
14+
api project(':server')
15+
api("junit:junit:${versions.junit}") {
16+
transitive = false
17+
}
18+
api project(':test:framework')
19+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
package fixture.aws.imds;
10+
11+
import com.sun.net.httpserver.HttpHandler;
12+
import com.sun.net.httpserver.HttpServer;
13+
14+
import org.junit.rules.ExternalResource;
15+
16+
import java.net.InetAddress;
17+
import java.net.InetSocketAddress;
18+
import java.net.UnknownHostException;
19+
import java.util.Objects;
20+
import java.util.Set;
21+
22+
public class Ec2ImdsHttpFixture extends ExternalResource {
23+
24+
private HttpServer server;
25+
26+
private final String accessKey;
27+
private final String sessionToken;
28+
private final Set<String> alternativeCredentialsEndpoints;
29+
30+
public Ec2ImdsHttpFixture(String accessKey, String sessionToken, Set<String> alternativeCredentialsEndpoints) {
31+
this.accessKey = accessKey;
32+
this.sessionToken = sessionToken;
33+
this.alternativeCredentialsEndpoints = alternativeCredentialsEndpoints;
34+
}
35+
36+
protected HttpHandler createHandler() {
37+
return new Ec2ImdsHttpHandler(accessKey, sessionToken, alternativeCredentialsEndpoints);
38+
}
39+
40+
public String getAddress() {
41+
return "http://" + server.getAddress().getHostString() + ":" + server.getAddress().getPort();
42+
}
43+
44+
public void stop(int delay) {
45+
server.stop(delay);
46+
}
47+
48+
protected void before() throws Throwable {
49+
server = HttpServer.create(resolveAddress(), 0);
50+
server.createContext("/", Objects.requireNonNull(createHandler()));
51+
server.start();
52+
}
53+
54+
@Override
55+
protected void after() {
56+
stop(0);
57+
}
58+
59+
private static InetSocketAddress resolveAddress() {
60+
try {
61+
return new InetSocketAddress(InetAddress.getByName("localhost"), 0);
62+
} catch (UnknownHostException e) {
63+
throw new RuntimeException(e);
64+
}
65+
}
66+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
package fixture.aws.imds;
10+
11+
import com.sun.net.httpserver.HttpExchange;
12+
import com.sun.net.httpserver.HttpHandler;
13+
14+
import org.elasticsearch.ExceptionsHelper;
15+
import org.elasticsearch.common.Strings;
16+
import org.elasticsearch.common.util.concurrent.ConcurrentCollections;
17+
import org.elasticsearch.core.SuppressForbidden;
18+
import org.elasticsearch.rest.RestStatus;
19+
20+
import java.io.IOException;
21+
import java.nio.charset.StandardCharsets;
22+
import java.time.Clock;
23+
import java.time.ZonedDateTime;
24+
import java.time.format.DateTimeFormatter;
25+
import java.util.Collection;
26+
import java.util.Objects;
27+
import java.util.Set;
28+
29+
import static org.elasticsearch.test.ESTestCase.randomIdentifier;
30+
31+
/**
32+
* Minimal HTTP handler that emulates the EC2 IMDS server
33+
*/
34+
@SuppressForbidden(reason = "this test uses a HttpServer to emulate the EC2 IMDS endpoint")
35+
public class Ec2ImdsHttpHandler implements HttpHandler {
36+
37+
private static final String IMDS_SECURITY_CREDENTIALS_PATH = "/latest/meta-data/iam/security-credentials/";
38+
39+
private final String accessKey;
40+
private final String sessionToken;
41+
private final Set<String> validCredentialsEndpoints = ConcurrentCollections.newConcurrentSet();
42+
43+
public Ec2ImdsHttpHandler(String accessKey, String sessionToken, Collection<String> alternativeCredentialsEndpoints) {
44+
this.accessKey = Objects.requireNonNull(accessKey);
45+
this.sessionToken = Objects.requireNonNull(sessionToken);
46+
this.validCredentialsEndpoints.addAll(alternativeCredentialsEndpoints);
47+
}
48+
49+
@Override
50+
public void handle(final HttpExchange exchange) throws IOException {
51+
// http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
52+
53+
try (exchange) {
54+
final var path = exchange.getRequestURI().getPath();
55+
final var requestMethod = exchange.getRequestMethod();
56+
57+
if ("PUT".equals(requestMethod) && "/latest/api/token".equals(path)) {
58+
// Reject IMDSv2 probe
59+
exchange.sendResponseHeaders(RestStatus.METHOD_NOT_ALLOWED.getStatus(), -1);
60+
return;
61+
}
62+
63+
if ("GET".equals(requestMethod)) {
64+
if (path.equals(IMDS_SECURITY_CREDENTIALS_PATH)) {
65+
final var profileName = randomIdentifier();
66+
validCredentialsEndpoints.add(IMDS_SECURITY_CREDENTIALS_PATH + profileName);
67+
final byte[] response = profileName.getBytes(StandardCharsets.UTF_8);
68+
exchange.getResponseHeaders().add("Content-Type", "text/plain");
69+
exchange.sendResponseHeaders(RestStatus.OK.getStatus(), response.length);
70+
exchange.getResponseBody().write(response);
71+
return;
72+
} else if (validCredentialsEndpoints.contains(path)) {
73+
final byte[] response = Strings.format(
74+
"""
75+
{
76+
"AccessKeyId": "%s",
77+
"Expiration": "%s",
78+
"RoleArn": "%s",
79+
"SecretAccessKey": "%s",
80+
"Token": "%s"
81+
}""",
82+
accessKey,
83+
ZonedDateTime.now(Clock.systemUTC()).plusDays(1L).format(DateTimeFormatter.ISO_DATE_TIME),
84+
randomIdentifier(),
85+
randomIdentifier(),
86+
sessionToken
87+
).getBytes(StandardCharsets.UTF_8);
88+
exchange.getResponseHeaders().add("Content-Type", "application/json");
89+
exchange.sendResponseHeaders(RestStatus.OK.getStatus(), response.length);
90+
exchange.getResponseBody().write(response);
91+
return;
92+
}
93+
}
94+
95+
ExceptionsHelper.maybeDieOnAnotherThread(new AssertionError("not supported: " + requestMethod + " " + path));
96+
}
97+
}
98+
}

0 commit comments

Comments
 (0)