From 561e098d02d2538cce5fb9ce319191df70f5a266 Mon Sep 17 00:00:00 2001 From: Jerry Duffy Date: Thu, 23 May 2024 08:30:27 -0400 Subject: [PATCH 1/2] More implementation --- .../agent/utilization/DockerData.java | 67 +++++++++++++++++-- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/utilization/DockerData.java b/newrelic-agent/src/main/java/com/newrelic/agent/utilization/DockerData.java index 6e2334fcee..f0c509f288 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/utilization/DockerData.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/utilization/DockerData.java @@ -8,12 +8,20 @@ package com.newrelic.agent.utilization; import com.newrelic.agent.Agent; +import org.apache.commons.lang3.StringUtils; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; +import java.io.IOException; +import java.io.InputStreamReader; import java.io.Reader; +import java.net.MalformedURLException; +import java.net.URL; import java.text.MessageFormat; import java.util.logging.Level; import java.util.regex.Matcher; @@ -31,6 +39,11 @@ * * We should grab the "cpu" line. The long id number is the number we want. * + * For AWS ECS (fargate and non-fargate) we check the metadata returned from the URL defined in either the + * v3 or v4 metadata URL. These checks are only made if the cgroup files don't return anything and the + * metadata URL(s) are present in the target env variables. The docker id returned in the metadata JSON response + * is a 32-digit hex followed by a 10-digit number in the "DockerId" key. + * * In either case, this is the full docker id, not the short id that appears when you run a "docker ps". */ public class DockerData { @@ -39,22 +52,40 @@ public class DockerData { private static final String FILE_WITH_CONTAINER_ID_V2 = "/proc/self/mountinfo"; private static final String CPU = "cpu"; + private static final String AWS_ECS_METADATA_V3_ENV_VAR = "ECS_CONTAINER_METADATA_URI"; + private static final String AWS_ECS_METADATA_V4_ENV_VAR = "ECS_CONTAINER_METADATA_URI_V4"; + private static final String FARGATE_DOCKER_ID_KEY = "DockerId"; + private static final Pattern VALID_CONTAINER_ID = Pattern.compile("^[0-9a-f]{64}$"); private static final Pattern DOCKER_CONTAINER_STRING_V1 = Pattern.compile("^.*[^0-9a-f]+([0-9a-f]{64,}).*"); private static final Pattern DOCKER_CONTAINER_STRING_V2 = Pattern.compile(".*/docker/containers/([0-9a-f]{64,}).*"); public String getDockerContainerId(boolean isLinux) { if (isLinux) { + String result; //try to get the container id from the v2 location File containerIdFileV2 = new File(FILE_WITH_CONTAINER_ID_V2); - String idResultV2 = getDockerIdFromFile(containerIdFileV2, CGroup.V2); - if (idResultV2 != null) { - return idResultV2; + result = getDockerIdFromFile(containerIdFileV2, CGroup.V2); + if (result != null) { + return result; } + //try to get container id from the v1 location File containerIdFileV1 = new File(FILE_WITH_CONTAINER_ID_V1); - return getDockerIdFromFile(containerIdFileV1, CGroup.V1); + result = getDockerIdFromFile(containerIdFileV1, CGroup.V1); + if (result != null) { + return result; + } + + // Try v4 ESC Fargate metadata call, the finally v3 + result = retrieveDockerIdFromFargateMetadata(System.getenv(AWS_ECS_METADATA_V4_ENV_VAR)); + if (result != null) { + return result; + } + + return retrieveDockerIdFromFargateMetadata(System.getenv(AWS_ECS_METADATA_V3_ENV_VAR)); } + return null; } @@ -153,5 +184,33 @@ private boolean checkAndGetMatch(Pattern p, StringBuilder result, String segment return false; } + private String retrieveDockerIdFromFargateMetadata(String metadataUrl) { + String dockerId = null; + StringBuffer jsonBlob = new StringBuffer(); + + if (StringUtils.isNotEmpty(metadataUrl)) { + try { + URL url = new URL(metadataUrl); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream()))) { + String line; + while ((line = reader.readLine()) != null) { + jsonBlob.append(line); + } + } + + JSONObject jsonObject = (JSONObject) new JSONParser().parse(jsonBlob.toString()); + dockerId = (String) jsonObject.get(FARGATE_DOCKER_ID_KEY); + } catch (MalformedURLException e) { + Agent.LOG.log(Level.FINEST, "Invalid AWS Fargate metadata URL: {0}", metadataUrl); + } catch (IOException e) { + Agent.LOG.log(Level.FINEST, "Error opening input stream for AWS Fargate metadata URL: {0}", metadataUrl); + } catch (ParseException e) { + Agent.LOG.log(Level.FINEST, "Error parsing JSON blob for AWS Fargate metadata URL: {0}", metadataUrl); + } + } + + return dockerId; + } } From 3d5a83beeae1a0f0e3feeb6717a1f203bfd6ee90 Mon Sep 17 00:00:00 2001 From: Jerry Duffy Date: Wed, 29 May 2024 15:40:00 -0400 Subject: [PATCH 2/2] Add tests --- .../AwsFargateMetadataFetcher.java | 27 +++++++ .../agent/utilization/DockerData.java | 59 +++++++++------- .../agent/utilization/DockerDataTest.java | 70 +++++++++++++++++++ 3 files changed, 129 insertions(+), 27 deletions(-) create mode 100644 newrelic-agent/src/main/java/com/newrelic/agent/utilization/AwsFargateMetadataFetcher.java diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/utilization/AwsFargateMetadataFetcher.java b/newrelic-agent/src/main/java/com/newrelic/agent/utilization/AwsFargateMetadataFetcher.java new file mode 100644 index 0000000000..3323df5c86 --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/utilization/AwsFargateMetadataFetcher.java @@ -0,0 +1,27 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.newrelic.agent.utilization; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; + +/** + * Thin wrapper class over the JRE URL class to assist in testing + */ +public class AwsFargateMetadataFetcher { + private final URL url; + + public AwsFargateMetadataFetcher(String metadataUrl) throws MalformedURLException { + url = new URL(metadataUrl); + } + + public InputStream openStream() throws IOException { + return (url == null ? null : url.openStream()); + } +} diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/utilization/DockerData.java b/newrelic-agent/src/main/java/com/newrelic/agent/utilization/DockerData.java index f0c509f288..71c03cf8fc 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/utilization/DockerData.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/utilization/DockerData.java @@ -7,8 +7,8 @@ package com.newrelic.agent.utilization; +import com.google.common.annotations.VisibleForTesting; import com.newrelic.agent.Agent; -import org.apache.commons.lang3.StringUtils; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import org.json.simple.parser.ParseException; @@ -21,7 +21,6 @@ import java.io.InputStreamReader; import java.io.Reader; import java.net.MalformedURLException; -import java.net.URL; import java.text.MessageFormat; import java.util.logging.Level; import java.util.regex.Matcher; @@ -77,13 +76,24 @@ public String getDockerContainerId(boolean isLinux) { return result; } - // Try v4 ESC Fargate metadata call, the finally v3 - result = retrieveDockerIdFromFargateMetadata(System.getenv(AWS_ECS_METADATA_V4_ENV_VAR)); - if (result != null) { - return result; - } + // Try v4 ESC Fargate metadata call, then finally v3 + String fargateUrl = null; + try { + fargateUrl = System.getenv(AWS_ECS_METADATA_V4_ENV_VAR); + if (fargateUrl != null) { + result = retrieveDockerIdFromFargateMetadata(new AwsFargateMetadataFetcher(fargateUrl)); + if (result != null) { + return result; + } + } - return retrieveDockerIdFromFargateMetadata(System.getenv(AWS_ECS_METADATA_V3_ENV_VAR)); + fargateUrl = System.getenv(AWS_ECS_METADATA_V3_ENV_VAR); + if (fargateUrl != null) { + return retrieveDockerIdFromFargateMetadata(new AwsFargateMetadataFetcher(fargateUrl)); + } + } catch (MalformedURLException e) { + Agent.LOG.log(Level.FINEST, "Invalid AWS Fargate metadata URL: {0}", fargateUrl); + } } return null; @@ -184,30 +194,25 @@ private boolean checkAndGetMatch(Pattern p, StringBuilder result, String segment return false; } - private String retrieveDockerIdFromFargateMetadata(String metadataUrl) { + @VisibleForTesting + String retrieveDockerIdFromFargateMetadata(AwsFargateMetadataFetcher awsFargateMetadataFetcher) { String dockerId = null; StringBuffer jsonBlob = new StringBuffer(); - if (StringUtils.isNotEmpty(metadataUrl)) { - try { - URL url = new URL(metadataUrl); - - try (BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream()))) { - String line; - while ((line = reader.readLine()) != null) { - jsonBlob.append(line); - } + try { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(awsFargateMetadataFetcher.openStream()))) { + String line; + while ((line = reader.readLine()) != null) { + jsonBlob.append(line); } - - JSONObject jsonObject = (JSONObject) new JSONParser().parse(jsonBlob.toString()); - dockerId = (String) jsonObject.get(FARGATE_DOCKER_ID_KEY); - } catch (MalformedURLException e) { - Agent.LOG.log(Level.FINEST, "Invalid AWS Fargate metadata URL: {0}", metadataUrl); - } catch (IOException e) { - Agent.LOG.log(Level.FINEST, "Error opening input stream for AWS Fargate metadata URL: {0}", metadataUrl); - } catch (ParseException e) { - Agent.LOG.log(Level.FINEST, "Error parsing JSON blob for AWS Fargate metadata URL: {0}", metadataUrl); } + + JSONObject jsonObject = (JSONObject) new JSONParser().parse(jsonBlob.toString()); + dockerId = (String) jsonObject.get(FARGATE_DOCKER_ID_KEY); + } catch (IOException e) { + Agent.LOG.log(Level.FINEST, "Error opening input stream retrieving AWS Fargate metadata"); + } catch (ParseException e) { + Agent.LOG.log(Level.FINEST, "Error parsing JSON blob for AWS Fargate metadata"); } return dockerId; diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/utilization/DockerDataTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/utilization/DockerDataTest.java index 528b67c8a3..8ecf70f84e 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/utilization/DockerDataTest.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/utilization/DockerDataTest.java @@ -20,14 +20,19 @@ import org.junit.Test; import org.mockito.Mockito; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.io.InputStream; import java.io.StringReader; import java.util.HashMap; import java.util.List; import java.util.Map; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + public class DockerDataTest { private final DockerData dockerData = new DockerData(); @@ -331,6 +336,40 @@ public void testInvalidDockerValueNull() { Assert.assertTrue(dockerData.isInvalidDockerValue(null)); } + @Test + public void retrieveDockerIdFromFargateMetadata_withValidUrl_returnsDockerId() throws IOException { + InputStream byteArrayStream = new ByteArrayInputStream(FARGATE_JSON.getBytes()); + AwsFargateMetadataFetcher mockFetcher = mock(AwsFargateMetadataFetcher.class); + when(mockFetcher.openStream()).thenReturn(byteArrayStream); + + DockerData dockerData = new DockerData(); + Assert.assertEquals("1e1698469422439ea356071e581e8545-2769485393", dockerData.retrieveDockerIdFromFargateMetadata(mockFetcher)); + } + + @Test + public void retrieveDockerIdFromFargateMetadata_withInvalidJson_returnsNull() throws IOException { + InputStream byteArrayStream = new ByteArrayInputStream("foofoo".getBytes()); + AwsFargateMetadataFetcher mockFetcher = mock(AwsFargateMetadataFetcher.class); + when(mockFetcher.openStream()).thenReturn(byteArrayStream); + + DockerData dockerData = new DockerData(); + Assert.assertNull(dockerData.retrieveDockerIdFromFargateMetadata(mockFetcher)); + } + + @Test + public void retrieveDockerIdFromFargateMetadata_withInputStreamException_returnsNull() throws IOException { + AwsFargateMetadataFetcher mockFetcher = mock(AwsFargateMetadataFetcher.class); + when(mockFetcher.openStream()).thenThrow(new IOException("oops")); + + DockerData dockerData = new DockerData(); + Assert.assertNull(dockerData.retrieveDockerIdFromFargateMetadata(mockFetcher)); + } + + @Test + public void getDockerContainerId_withNoDockerIdSource_returnsNull() { + Assert.assertNull(dockerData.getDockerContainerId(true)); + } + private void processFile(File file, String answer, CGroup cgroup) { System.out.println("Current test file: " + file.getAbsolutePath()); String actual = dockerData.getDockerIdFromFile(file, cgroup); @@ -375,4 +414,35 @@ private JSONArray readJsonAndGetTests(File file) throws Exception { return theTests; } + private static final String FARGATE_JSON = + "{" + + "\"DockerId\": \"1e1698469422439ea356071e581e8545-2769485393\"," + + "\"Name\": \"fargateapp\"," + + "\"DockerName\": \"fargateapp\"," + + "\"Image\": \"123456789012.dkr.ecr.us-west-2.amazonaws.com/fargatetest:latest\"," + + "\"ImageID\": \"sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd\"," + + "\"Labels\": {" + + "\"com.amazonaws.ecs.cluster\": \"arn:aws:ecs:us-west-2:123456789012:cluster/testcluster\"," + + "\"com.amazonaws.ecs.container-name\": \"fargateapp\"," + + "\"com.amazonaws.ecs.task-arn\": \"arn:aws:ecs:us-west-2:123456789012:task/testcluster/1e1698469422439ea356071e581e8545\"," + + "\"com.amazonaws.ecs.task-definition-family\": \"fargatetestapp\"," + + "\"com.amazonaws.ecs.task-definition-version\": \"7\"" + + "}," + + "\"DesiredStatus\": \"RUNNING\"," + + "\"KnownStatus\": \"RUNNING\"," + + "\"Limits\": {" + + "\"CPU\": 2" + + "}," + + "\"CreatedAt\": \"2024-04-25T17:38:31.073208914Z\"," + + "\"StartedAt\": \"2024-04-25T17:38:31.073208914Z\"," + + "\"Type\": \"NORMAL\"," + + "\"Networks\": [" + + "{" + + "\"NetworkMode\": \"awsvpc\"," + + "\"IPv4Addresses\": [" + + "\"10.10.10.10\"" + + "]" + + "}" + + "]" + + "}"; }