Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fetch the container ID for ECS Fargate instances #1952

Merged
merged 2 commits into from
Jun 20, 2024
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
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,20 @@

package com.newrelic.agent.utilization;

import com.google.common.annotations.VisibleForTesting;
import com.newrelic.agent.Agent;
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.text.MessageFormat;
import java.util.logging.Level;
import java.util.regex.Matcher;
Expand All @@ -31,6 +38,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 {
Expand All @@ -39,22 +51,51 @@ 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, 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;
}
}

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;
}

Expand Down Expand Up @@ -153,5 +194,28 @@ private boolean checkAndGetMatch(Pattern p, StringBuilder result, String segment
return false;
}

@VisibleForTesting
String retrieveDockerIdFromFargateMetadata(AwsFargateMetadataFetcher awsFargateMetadataFetcher) {
String dockerId = null;
StringBuffer jsonBlob = new StringBuffer();

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 (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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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\"" +
"]" +
"}" +
"]" +
"}";
}
Loading