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

parse container ID with cgroups v2 #3199

Merged
merged 8 commits into from
Jul 10, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
Expand Up @@ -43,11 +43,18 @@
public class SystemInfo {
private static final Logger logger = LoggerFactory.getLogger(SystemInfo.class);

private static final String CONTAINER_UID_REGEX = "^[0-9a-fA-F]{64}$";
private static final String CONTAINER_REGEX_64 = "[0-9a-fA-F]{64}";
private static final String CONTAINER_UID_REGEX = "^" + CONTAINER_REGEX_64 + "$";
private static final String SHORTENED_UUID_PATTERN = "^[0-9a-fA-F]{8}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4,}";
private static final String AWS_FARGATE_UID_REGEX = "^[0-9a-fA-F]{32}\\-[0-9]{10}$";
private static final String POD_REGEX = "(?:^/kubepods[\\S]*/pod([^/]+)$)|(?:kubepods[^/]*-pod([^/]+)\\.slice)";

private static final String CGROUPV2_HOSTNAME_FILE = "/etc/hostname";
private static final Pattern CGROUPV2_CONTAINER_PATTERN = Pattern.compile("^.*(" + CONTAINER_REGEX_64 + ").*$");

private static final String SELF_CGROUP = "/proc/self/cgroup";
private static final String SELF_MOUNTINFO = "/proc/self/mountinfo";

/**
* Architecture of the system the agent is running on.
*/
Expand All @@ -56,6 +63,7 @@ public class SystemInfo {
/**
* Hostname configured manually through {@link co.elastic.apm.agent.configuration.CoreConfiguration#hostname}.
*/
@SuppressWarnings("JavadocReference")
@Nullable
private final String configuredHostname;

Expand Down Expand Up @@ -99,11 +107,13 @@ public SystemInfo(String architecture, @Nullable String configuredHostname, @Nul
/**
* Creates a {@link SystemInfo} containing auto-discovered info about the system.
* This method may block on reading files and executing external processes.
* @param configuredHostname hostname configured through the {@link co.elastic.apm.agent.configuration.CoreConfiguration#hostname} config
* @param timeoutMillis enables to limit the execution of the system discovery task
*
* @param configuredHostname hostname configured through the {@link co.elastic.apm.agent.configuration.CoreConfiguration#hostname} config
* @param timeoutMillis enables to limit the execution of the system discovery task
* @param serverlessConfiguration serverless config
* @return a future from which this system's info can be obtained
*/
@SuppressWarnings("JavadocReference")
public static SystemInfo create(final @Nullable String configuredHostname, final long timeoutMillis, ServerlessConfiguration serverlessConfiguration) {
final String osName = System.getProperty("os.name");
final String osArch = System.getProperty("os.arch");
Expand Down Expand Up @@ -134,7 +144,8 @@ static boolean isWindows(String osName) {
* Discover the current host's name. This method separates operating systems only to Windows and non-Windows,
* both in the executed hostname-discovery-command and the fallback environment variables.
* It always starts with execution of a command on an external process, so it may block up to the specified timeout.
* @param isWindows used to decide how hostname discovery should be executed
*
* @param isWindows used to decide how hostname discovery should be executed
* @param timeoutMillis limits the time this method may block on executing external commands
* @return the discovered hostname
*/
Expand Down Expand Up @@ -193,7 +204,8 @@ static String discoverHostnameThroughCommand(boolean isWindows, long timeoutMill
/**
* Tries to discover the current host name by executing the provided command in a spawned process.
* This method may block up to the specified timeout, waiting for the spawned process to terminate.
* @param cmd the hostname discovery command
*
* @param cmd the hostname discovery command
* @param timeoutMillis maximum time to allow to the provided command to execute
* @return the discovered hostname
*/
Expand Down Expand Up @@ -259,21 +271,9 @@ static String removeDomain(String hostname) {
* @return container ID parsed from {@code /proc/self/cgroup} file lines, or {@code null} if can't find/read/parse file lines
*/
SystemInfo findContainerDetails() {
String containerId = null;
try {
Path path = FileSystems.getDefault().getPath("/proc/self/cgroup");
if (path.toFile().exists()) {
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
for (final String line : lines) {
parseContainerId(line);
if (container != null) {
containerId = container.getId();
break;
}
}
}
} catch (Throwable e) {
logger.warn("Failed to read/parse container ID from '/proc/self/cgroup'", e);
parseCgroupsFile(FileSystems.getDefault().getPath(SELF_CGROUP));
if (container == null) {
parseMountInfo(FileSystems.getDefault().getPath(SELF_MOUNTINFO));
}

try {
Expand All @@ -297,29 +297,68 @@ SystemInfo findContainerDetails() {
logger.warn("Failed to read environment variables for Kubernetes Downward API discovery", e);
}

logger.debug("container ID is {}", containerId);
logger.debug("container ID is {}", container != null ? container.getId() : null);
return this;
}

@Nullable
private void parseMountInfo(Path path) {
if (!Files.isRegularFile(path)) {
logger.debug("Could not parse container ID from '{}'", path);
return;
}
try {
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
parseCgroupsV2ContainerId(lines);
if (container != null) {
return;
}
logger.debug("Could not parse container ID from '{}' lines: {}", path, lines);
} catch (Throwable e) {
logger.warn(String.format("Failed to read/parse container ID from '%s'", path), e);
}
}

@Nullable
private void parseCgroupsFile(Path path) {
if(!Files.isRegularFile(path)){
logger.debug("Could not parse container ID from '{}'", path);
return;
}
try {
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
for (String line : lines) {
parseCgroupsLine(line);
if (container != null) {
return;
}
}
} catch (Throwable e) {
logger.warn(String.format("Failed to read/parse container ID from '%s'", path), e);
}
}

/**
* The virtual file /proc/self/cgroup lists the control groups that the process is a member of. Each line contains
* three colon-separated fields of the form hierarchy-ID:subsystem-list:cgroup-path.
*
* <p>
* Depending on the filesystem driver used for cgroup management, the cgroup-path will have
* one of the following formats in a Docker container:
*
* systemd: /system.slice/docker-<container-ID>.scope
* cgroupfs: /docker/<container-ID>
*
* In a Kubernetes pod, the cgroup path will look like:
*
* systemd: /kubepods.slice/kubepods-<QoS-class>.slice/kubepods-<QoS-class>-pod<pod-UID>.slice/<container-iD>.scope
* cgroupfs: /kubepods/<QoS-class>/pod<pod-UID>/<container-iD>
* </p>
* <pre>
* systemd: /system.slice/docker-<container-ID>.scope
* cgroupfs: /docker/<container-ID>
* </pre>
* In a Kubernetes pod, the cgroup path will look like:
* <pre>
* systemd: /kubepods.slice/kubepods-<QoS-class>.slice/kubepods-<QoS-class>-pod<pod-UID>.slice/<container-iD>.scope
* cgroupfs: /kubepods/<QoS-class>/pod<pod-UID>/<container-iD>
* </pre
*
* @param line a line from the /proc/self/cgroup file
* @return this SystemInfo object after parsing
*/
SystemInfo parseContainerId(String line) {
SystemInfo parseCgroupsLine(String line) {
final String[] fields = line.split(":", 3);
if (fields.length == 3) {
String cGroupPath = fields[2];
Expand Down Expand Up @@ -364,14 +403,38 @@ SystemInfo parseContainerId(String line) {
if (kubernetes != null ||
idPart.matches(CONTAINER_UID_REGEX) ||
idPart.matches(SHORTENED_UUID_PATTERN) ||
idPart.matches(AWS_FARGATE_UID_REGEX)) {
idPart.matches(AWS_FARGATE_UID_REGEX)) {
container = new Container(idPart);
}
}
}
if (container == null) {
logger.debug("Could not parse container ID from '/proc/self/cgroup' line: {}", line);
logger.debug("Could not parse container ID from line: {}", line);
}
return this;
}

/**
* @param lines lines from the /proc/self/mountinfo file
* @return this SystemInfo object after parsing
*/
SystemInfo parseCgroupsV2ContainerId(List<String> lines) {
for (String line : lines) {
int index = line.indexOf(CGROUPV2_HOSTNAME_FILE);
if (index > 0) {
String[] parts = line.split(" ");
if (parts.length > 3) {
Matcher matcher = CGROUPV2_CONTAINER_PATTERN.matcher(parts[3]);
if (matcher.matches() && matcher.groupCount() == 1) {
container = new Container(matcher.group(1));
}
}
}
}




return this;
}

Expand All @@ -386,23 +449,26 @@ public String getArchitecture() {
* Returns the hostname. If a non-empty hostname was configured manually, it will be returned.
* Otherwise, the automatically discovered hostname will be returned.
* If both are null or empty, this method returns {@code <unknown>}.
*
* @deprecated should only be used when communicating to APM Server of version lower than 7.4
*/
@Deprecated
@Deprecated
public String getHostname() {
if (configuredHostname != null && !configuredHostname.isEmpty()) {
return configuredHostname;
}
if (detectedHostname != null && !detectedHostname.isEmpty()) {
return detectedHostname;
}
return "<unknown>";
if (configuredHostname != null && !configuredHostname.isEmpty()) {
return configuredHostname;
}
if (detectedHostname != null && !detectedHostname.isEmpty()) {
return detectedHostname;
}
return "<unknown>";
}

/**
* The hostname manually configured through {@link co.elastic.apm.agent.configuration.CoreConfiguration#hostname}
*
* @return the manually configured hostname
*/
@SuppressWarnings("JavadocReference")
@Nullable
public String getConfiguredHostname() {
return configuredHostname;
Expand Down Expand Up @@ -444,7 +510,7 @@ public Kubernetes getKubernetesInfo() {
}

public static class Container {
private String id;
private final String id;

Container(String id) {
this.id = id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ public class ContainerInfoTest extends CustomEnvVariables {

@ParameterizedTest(name = "{0}")
@MethodSource("getCommonPatterns")
void testCommonPatterns(String testName, String groupLine, @Nullable String containerId, @Nullable String podId) {
SystemInfo systemInfo = createSystemInfo().parseContainerId(groupLine);
void testCommonPatternsCgroupsV1(String testName, String groupLine, @Nullable String containerId, @Nullable String podId) {
SystemInfo systemInfo = createSystemInfo().parseCgroupsLine(groupLine);
assertThat(systemInfo).isNotNull();
SystemInfo.Container containerInfo = systemInfo.getContainerInfo();
if (containerId == null) {
Expand Down Expand Up @@ -83,6 +83,29 @@ static Stream<Arguments> getCommonPatterns() {
return args.stream();
}

@Test
void testCgroupsV2() {
JsonNode json = TestJsonSpec.getJson("mounts_parsing.json");
assertThat(json.isObject())
.describedAs("unexpected JSON spec format")
.isTrue();

JsonNode jsonLines = json.get("lines");
assertThat(jsonLines.isArray()).isTrue();
List<String> lines = new ArrayList<>();
for (int i = 0; i < jsonLines.size(); i++) {
lines.add(jsonLines.get(i).asText());
}
JsonNode jsonContainerId = json.get("containerId");
assertThat(jsonContainerId.isTextual()).isTrue();
String expectedContainerId = jsonContainerId.asText();

SystemInfo systemInfo = createSystemInfo().parseCgroupsV2ContainerId(lines);
assertThat(systemInfo).isNotNull();
assertThat(systemInfo.getContainerInfo()).describedAs("missing container info").isNotNull();
assertThat(systemInfo.getContainerInfo().getId()).isEqualTo(expectedContainerId);
}

@Test
void testContainerIdParsing() {
String validId = "3741401135a8d27237e2fb9c0fb2ecd93922c0d1dd708345451e479613f8d4ae";
Expand Down Expand Up @@ -168,7 +191,7 @@ void testKubernetesInfo() {
void testUbuntuCgroup() {
String line = "1:name=systemd:/user.slice/user-1000.slice/[email protected]/apps.slice/apps-org.gnome.Terminal" +
".slice/vte-spawn-75bc72bd-6642-4cf5-b62c-0674e11bfc84.scope";
assertThat(createSystemInfo().parseContainerId(line).getContainerInfo()).isNull();
assertThat(createSystemInfo().parseCgroupsLine(line).getContainerInfo()).isNull();
}

@Test
Expand Down Expand Up @@ -237,15 +260,15 @@ private SystemInfo createSystemInfo() {

private SystemInfo assertContainerId(String line, String containerId) {
SystemInfo systemInfo = createSystemInfo();
assertThat(systemInfo.parseContainerId(line).getContainerInfo()).isNotNull();
assertThat(systemInfo.parseCgroupsLine(line).getContainerInfo()).isNotNull();
//noinspection ConstantConditions
assertThat(systemInfo.getContainerInfo().getId()).isEqualTo(containerId);
return systemInfo;
}

private void assertContainerInfoIsNull(String line) {
SystemInfo systemInfo = createSystemInfo();
assertThat(systemInfo.parseContainerId(line).getContainerInfo()).isNull();
assertThat(systemInfo.parseCgroupsLine(line).getContainerInfo()).isNull();
}

private void assertKubernetesInfo(SystemInfo systemInfo, @Nullable String podUid, @Nullable String podName, @Nullable String nodeName,
Expand Down
20 changes: 20 additions & 0 deletions apm-agent-core/src/test/resources/json-specs/mounts_parsing.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"lines": [
"3984 3905 0:73 / / rw,relatime shared:1863 master:1733 - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/KEX7CWLHQCXQY2RHPGTXJ3C26N:/var/lib/docker/overlay2/l/2PVS7JRTRSTVZS4KSUAFML3BIV:/var/lib/docker/overlay2/l/52M7ARM4JDVHCJAYUI6JIKBO4B,upperdir=/var/lib/docker/overlay2/267f825fb89e584605bf161177451879c0ba8b15f7df9b51fb7843c7beb9ed25/diff,workdir=/var/lib/docker/overlay2/267f825fb89e584605bf161177451879c0ba8b15f7df9b51fb7843c7beb9ed25/work",
"3985 3984 0:77 / /proc rw,nosuid,nodev,noexec,relatime shared:1864 - proc proc rw",
"3986 3984 0:78 / /dev rw,nosuid shared:1865 - tmpfs tmpfs rw,size=65536k,mode=755,inode64",
"3987 3986 0:79 / /dev/pts rw,nosuid,noexec,relatime shared:1866 - devpts devpts rw,gid=5,mode=620,ptmxmode=666",
"3988 3984 0:80 / /sys ro,nosuid,nodev,noexec,relatime shared:1870 - sysfs sysfs ro",
"3989 3988 0:30 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime shared:1871 - cgroup2 cgroup rw",
"3990 3986 0:76 / /dev/mqueue rw,nosuid,nodev,noexec,relatime shared:1867 - mqueue mqueue rw",
"3991 3986 0:81 / /dev/shm rw,nosuid,nodev,noexec,relatime shared:1868 - tmpfs shm rw,size=65536k,inode64",
"3992 3984 253:1 /var/lib/docker/volumes/9d18ce5b36572d85358fa936afe5a4bf95cca5c822b04941aa08c6118f6e0d33/_data /var rw,relatime shared:1872 master:1 - ext4 /dev/mapper/vgubuntu-root rw,errors=remount-ro",
"3993 3984 0:82 / /run rw,nosuid,nodev,noexec,relatime shared:1873 - tmpfs tmpfs rw,inode64",
"3994 3984 0:83 / /tmp rw,nosuid,nodev,noexec,relatime shared:1874 - tmpfs tmpfs rw,inode64",
"3995 3984 253:1 /usr/lib/modules /usr/lib/modules ro,relatime shared:1875 - ext4 /dev/mapper/vgubuntu-root rw,errors=remount-ro",
"3996 3984 253:1 /var/lib/docker/containers/6548c6863fb748e72d1e2a4f824fde92f720952d062dede1318c2d6219a672d6/resolv.conf /etc/resolv.conf rw,relatime shared:1876 - ext4 /dev/mapper/vgubuntu-root rw,errors=remount-ro",
"3997 3984 253:1 /var/lib/docker/containers/6548c6863fb748e72d1e2a4f824fde92f720952d062dede1318c2d6219a672d6/hostname /etc/hostname rw,relatime shared:1877 - ext4 /dev/mapper/vgubuntu-root rw,errors=remount-ro",
"3998 3984 253:1 /var/lib/docker/containers/6548c6863fb748e72d1e2a4f824fde92f720952d062dede1318c2d6219a672d6/hosts /etc/hosts rw,relatime shared:1878 - ext4 /dev/mapper/vgubuntu-root rw,errors=remount-ro"
],
"containerId": "6548c6863fb748e72d1e2a4f824fde92f720952d062dede1318c2d6219a672d6"
}