Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions core/imds/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@
<version>${awsjavasdk.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>json-utils</artifactId>
<version>${awsjavasdk.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>aws-json-protocol</artifactId>
<version>${awsjavasdk.version}</version>
<scope>compile</scope>
</dependency>
Comment on lines +102 to +107
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should put something in the backlog to avoid needing this dependency at GA. I know it was needed for the DocumentMarshaller, but I think it's relatively small and we could copy it to avoid the dependency.

<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>test-utils</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ public interface Ec2Metadata {
/**
* Gets the specified instance metadata value by the given path.
* @param path Input path
* @return Instance metadata value
* @return Instance metadata value as part of MetadataResponse Object
*/
String get(String path);
MetadataResponse get(String path);

/**
* @return The Builder Object consisting all the fields.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.imds;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.core.document.Document;
import software.amazon.awssdk.protocols.json.internal.unmarshall.document.DocumentUnmarshaller;
import software.amazon.awssdk.protocols.jsoncore.JsonNode;
import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser;
import software.amazon.awssdk.utils.Validate;

/**
* The class is used for Response Handling and Parsing the metadata fetched by the get call in the {@link Ec2Metadata} interface.
* The class provides convenience methods to the users to parse the metadata as a String, List and Document.
*/
@SdkPublicApi
public class MetadataResponse {

private static final Logger log = LoggerFactory.getLogger(MetadataResponse.class);

private static final JsonNodeParser JSON_NODE_PARSER = JsonNode.parserBuilder().removeErrorLocations(true).build();

private final String body;

public MetadataResponse(String body) {
this.body = Validate.notNull(body, "Metadata is null");
}

/**
* Returns the Metadata Response body as a String. This method can be used for parsing the retrieved
* singular metadata from IMDS.
*
* @return String Representation of the Metadata Response Body.
*
* <p>
* Example:
* <pre>
* {@code
*
* Ec2Metadata ec2Metadata = Ec2Metadata.create();
* MetadataResponse metadataResponse = client.get("/latest/meta-data/ami-id");
* String response = metadataResponse.asString();
* }
* </pre>
*/
public String asString() {
return body;

}

/**
* Parses the response String into a list of Strings split by delimiter ("\n"). This method can be used for parsing the
* list-type metadata from IMDS.
*
* @return List Representation of the Metadata Response Body.
*
* <p>
* Example:
* <pre>
* {@code
*
* Ec2Metadata ec2Metadata = Ec2Metadata.create();
* MetadataResponse metadataResponse = client.get("/latest/meta-data/ancestor-ami-ids");
* List<String>response = metadataResponse.asList();
* }
* </pre>
*/
public List<String> asList() {
return Arrays.asList(body.split("\n"));
}


/**
* Parses the response String into {@link Document} type. This method can be used for
* parsing the metadata in a String Json Format.
*
* @return Document Representation of the Metadata Response Body.
* @throws IOException in case parsing does not happen correctly.
*
* <p>
* Example:
* <pre>
* {@code
*
* Ec2Metadata ec2Metadata = Ec2Metadata.create();
* MetadataResponse metadataResponse = client.get("/latest/dynamic/instance-identity/document");
* Document document = metadataResponse.asDocument();
* }
* </pre>
*/

public Document asDocument() throws IOException {

JsonNode node = JSON_NODE_PARSER.parse(body);
return node.visit(new DocumentUnmarshaller());
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import software.amazon.awssdk.http.SdkHttpMethod;
import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
import software.amazon.awssdk.imds.Ec2Metadata;
import software.amazon.awssdk.imds.MetadataResponse;
import software.amazon.awssdk.utils.IoUtils;

/**
Expand Down Expand Up @@ -145,11 +146,12 @@ public String toString() {
/**
* Gets the specified instance metadata value by the given path.
* @param path Input path
* @return Instance metadata value
* @return Instance metadata value as part of MetadataResponse Object
*/
@Override
public String get(String path) {
public MetadataResponse get(String path) {

MetadataResponse metadataResponse = null;
String data = null;
AbortableInputStream abortableInputStream = null;
try {
Expand All @@ -164,6 +166,7 @@ public String get(String path) {
if (statusCode == HttpURLConnection.HTTP_OK && responseBody.isPresent()) {
abortableInputStream = responseBody.get();
data = IoUtils.toUtf8String(abortableInputStream);
metadataResponse = new MetadataResponse(data);
} else if (statusCode == HttpURLConnection.HTTP_NOT_FOUND) {
throw SdkServiceException.builder()
.message("The requested metadata at path ( " + path + " ) is not found ").build();
Expand All @@ -184,7 +187,7 @@ public String get(String path) {
IoUtils.closeQuietly(abortableInputStream, log);
}

return data;
return metadataResponse;
}

private String getToken() throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
import org.mockito.junit.MockitoJUnitRunner;
import software.amazon.awssdk.core.SdkSystemSetting;
import software.amazon.awssdk.core.exception.SdkServiceException;
import software.amazon.awssdk.http.ExecutableHttpRequest;
import software.amazon.awssdk.protocols.jsoncore.JsonNode;
import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser;

/**
* Unit Tests to test the Ec2Metadata Client functionality
Expand All @@ -65,6 +66,8 @@ public class Ec2MetadataTest {
@Rule
public WireMockRule mockMetadataEndpoint = new WireMockRule();

private static final JsonNodeParser jsonParser = JsonNode.parser();

@Before
public void methodSetup() {
System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property(), "http://localhost:" + mockMetadataEndpoint.port());
Expand All @@ -74,8 +77,9 @@ public void methodSetup() {
@Test
public void when_dummy_string_is_returned(){

when(ec2Metadata.get("/ami-id")).thenReturn("IMDS");
assertThat(ec2Metadata.get("/ami-id")).isEqualTo("IMDS");
MetadataResponse metadataResponse = new MetadataResponse("IMDS");
when(ec2Metadata.get("/ami-id")).thenReturn(metadataResponse);
assertThat(ec2Metadata.get("/ami-id").asString()).isEqualTo("IMDS");

}

Expand All @@ -93,9 +97,9 @@ public void get_AmiId_onMetadataResource_200_Success() throws IOException {
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}")));

Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
String data = ec2Metadata.get("/latest/meta-data/ami-id");

assertThat(data).isEqualTo("{}");
MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
assertThat(metadataResponse.asString()).isEqualTo("{}");

WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
WireMock.verify(getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)).withHeader(TOKEN_HEADER, equalTo("some-token")));
Expand All @@ -111,7 +115,7 @@ public void get_AmiId_onMetadataResource_404Error_throws() throws IOException {
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}").withStatus(404)));

Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
String data = ec2Metadata.get("/latest/meta-data/ami-id");
MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
}

@Test
Expand All @@ -121,9 +125,8 @@ public void get_AmiId_onMetadataResource_401Error_throws() throws IOException {
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}").withStatus(401)));

Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
String data = ec2Metadata.get("/latest/meta-data/ami-id");

assertThat(data).isNull();
MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
assertThat(metadataResponse).isNull();
Comment on lines +128 to +129
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning null seems weird here. Any reason we wouldn't throw an exception? I'd think we'd never want get() to return null.

}

@Test
Expand All @@ -133,9 +136,9 @@ public void get_AmiId_onMetadataResource_IOException_throws() {
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withFixedDelay(Integer.MAX_VALUE)));

Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
String data = ec2Metadata.get("/latest/meta-data/ami-id");

assertThat(data).isNull();
MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
assertThat(metadataResponse).isNull();

WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
WireMock.verify(getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)).withHeader(TOKEN_HEADER, equalTo("some-token")));
Expand All @@ -151,7 +154,7 @@ public void get_AmiId_onTokenResource_403Error_throws() throws IOException {
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}")));

Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
String data = ec2Metadata.get("/latest/meta-data/ami-id");
MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
}

@Test
Expand All @@ -161,9 +164,9 @@ public void get_AmiId_onTokenResource_401Error_throws() throws IOException {
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}")));

Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
String data = ec2Metadata.get("/latest/meta-data/ami-id");

assertThat(data).isNull();
MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
assertThat(metadataResponse).isNull();
WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));

}
Expand All @@ -174,22 +177,20 @@ public void getAmiId_onTokenResource_IOError_throws() throws IOException {
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}")));

Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
String data = ec2Metadata.get("/latest/meta-data/ami-id");

assertThat(data).isNull();
MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
assertThat(metadataResponse).isNull();

WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
// WireMock.verify(getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)).withHeader(TOKEN_HEADER, equalTo("some-token")));
}
@Test
public void getAmiId_onTokenResource_200() throws IOException {
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token")));
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}")));

Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
String data = ec2Metadata.get("/latest/meta-data/ami-id");

assertThat(data).isEqualTo("{}");
MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
assertThat(metadataResponse.asString()).isEqualTo("{}");

WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
WireMock.verify(getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)).withHeader(TOKEN_HEADER, equalTo("some-token")));
Expand Down
Loading