diff --git a/core/imds/pom.xml b/core/imds/pom.xml index 68b2a57e5894..b0cfde3490a8 100644 --- a/core/imds/pom.xml +++ b/core/imds/pom.xml @@ -93,6 +93,18 @@ ${awsjavasdk.version} compile + + software.amazon.awssdk + json-utils + ${awsjavasdk.version} + compile + + + software.amazon.awssdk + aws-json-protocol + ${awsjavasdk.version} + compile + software.amazon.awssdk test-utils diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2Metadata.java b/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2Metadata.java index d40150d50f30..1d89b27c6bb1 100644 --- a/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2Metadata.java +++ b/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2Metadata.java @@ -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. diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/MetadataResponse.java b/core/imds/src/main/java/software/amazon/awssdk/imds/MetadataResponse.java new file mode 100644 index 000000000000..eefd16cfe8ce --- /dev/null +++ b/core/imds/src/main/java/software/amazon/awssdk/imds/MetadataResponse.java @@ -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. + * + *

+ * Example: + *

+     * {@code
+     *
+     * Ec2Metadata ec2Metadata = Ec2Metadata.create();
+     * MetadataResponse metadataResponse = client.get("/latest/meta-data/ami-id");
+     * String response = metadataResponse.asString();
+     *  }
+     *  
+ */ + 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. + * + *

+ * Example: + *

+     * {@code
+     *
+     * Ec2Metadata ec2Metadata = Ec2Metadata.create();
+     * MetadataResponse metadataResponse = client.get("/latest/meta-data/ancestor-ami-ids");
+     * Listresponse = metadataResponse.asList();
+     * }
+     * 
+ */ + public List 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. + * + *

+ * Example: + *

+     * {@code
+     *
+     * Ec2Metadata ec2Metadata = Ec2Metadata.create();
+     * MetadataResponse metadataResponse = client.get("/latest/dynamic/instance-identity/document");
+     * Document document = metadataResponse.asDocument();
+     * }
+     * 
+ */ + + public Document asDocument() throws IOException { + + JsonNode node = JSON_NODE_PARSER.parse(body); + return node.visit(new DocumentUnmarshaller()); + } + + +} diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/internal/DefaultEc2Metadata.java b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/DefaultEc2Metadata.java index da9335da5520..5d1d415d9b2e 100644 --- a/core/imds/src/main/java/software/amazon/awssdk/imds/internal/DefaultEc2Metadata.java +++ b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/DefaultEc2Metadata.java @@ -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; /** @@ -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 { @@ -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(); @@ -184,7 +187,7 @@ public String get(String path) { IoUtils.closeQuietly(abortableInputStream, log); } - return data; + return metadataResponse; } private String getToken() throws IOException { diff --git a/core/imds/src/test/java/software/amazon/awssdk/imds/Ec2MetadataTest.java b/core/imds/src/test/java/software/amazon/awssdk/imds/Ec2MetadataTest.java index cc3a1f2d53dd..70bf073ed144 100644 --- a/core/imds/src/test/java/software/amazon/awssdk/imds/Ec2MetadataTest.java +++ b/core/imds/src/test/java/software/amazon/awssdk/imds/Ec2MetadataTest.java @@ -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 @@ -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()); @@ -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"); } @@ -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"))); @@ -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 @@ -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(); } @Test @@ -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"))); @@ -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 @@ -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"))); } @@ -174,12 +177,10 @@ 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 { @@ -187,9 +188,9 @@ public void getAmiId_onTokenResource_200() 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"))); diff --git a/core/imds/src/test/java/software/amazon/awssdk/imds/MetadataResponseTest.java b/core/imds/src/test/java/software/amazon/awssdk/imds/MetadataResponseTest.java new file mode 100644 index 000000000000..e41050922315 --- /dev/null +++ b/core/imds/src/test/java/software/amazon/awssdk/imds/MetadataResponseTest.java @@ -0,0 +1,121 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import software.amazon.awssdk.core.document.Document; + +/** + * The class tests the utility methods provided by MetadataResponse Class . + */ +public class MetadataResponseTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void check_asString_success() throws IOException { + + String response = "foobar"; + + MetadataResponse metadataResponse = new MetadataResponse(response); + String result = metadataResponse.asString(); + assertThat(result).isEqualTo(response); + + } + + @Test + public void check_asString_failure() throws IOException { + + thrown.expect(NullPointerException.class); + thrown.expectMessage("Metadata is null"); + + MetadataResponse metadataResponse = new MetadataResponse(null); + String result = metadataResponse.asString(); + } + + @Test + public void check_asList_success_with_delimiter() throws IOException { + + String response = "sai\ntest"; + + MetadataResponse metadataResponse = new MetadataResponse(response); + List result = metadataResponse.asList(); + assertThat(result).hasSize(2); + + } + + @Test + public void check_asList_success_without_delimiter() throws IOException { + + String response = "test1-test2"; + + MetadataResponse metadataResponse = new MetadataResponse(response); + List result = metadataResponse.asList(); + assertThat(result).hasSize(1); + + } + @Test + public void check_asList_failure() throws IOException { + + thrown.expect(NullPointerException.class); + thrown.expectMessage("Metadata is null"); + + MetadataResponse metadataResponse = new MetadataResponse(null); + List result = metadataResponse.asList(); + } + + @Test + public void check_asDocument_failure() throws IOException { + thrown.expect(NullPointerException.class); + thrown.expectMessage("Metadata is null"); + + MetadataResponse metadataResponse = new MetadataResponse(null); + Document document = metadataResponse.asDocument(); + + } + + @Test + public void check_asDocument_success() throws IOException { + String jsonResponse = "{" + + "\"instanceType\":\"m1.small\"," + + "\"devpayProductCodes\":[\"bar\",\"foo\"]" + + "}"; + + MetadataResponse metadataResponse = new MetadataResponse(jsonResponse); + Document document = metadataResponse.asDocument(); + Map expectedMap = new LinkedHashMap<>(); + + List documentList = new ArrayList<>(); + documentList.add(Document.fromString("bar")); + documentList.add(Document.fromString("foo")); + + expectedMap.put("instanceType", Document.fromString("m1.small")); + expectedMap.put("devpayProductCodes", Document.fromList(documentList)); + Document expectedDocumentMap = Document.fromMap(expectedMap); + assertThat(document).isEqualTo(expectedDocumentMap); + } + +}