record ManagedInstanceInformation(boolean isManaged, @Nullable String provider) {
}
- record CloudUsage(boolean kubernetes, @Nullable String kubernetesVersion, @Nullable String kubernetesPlatform,
- @Nullable String kubernetesProvider, @Nullable String officialHelmChart, boolean officialImage) {
+ record CloudUsage(boolean kubernetes, @Nullable String kubernetesVersion, @Nullable String kubernetesPlatform, @Nullable String kubernetesProvider,
+ @Nullable String officialHelmChart, @Nullable String containerRuntime, boolean officialImage) {
}
public static class ProjectStatistics {
json.prop("kubernetesPlatform", cloudUsage.kubernetesPlatform());
json.prop("kubernetesProvider", cloudUsage.kubernetesProvider());
json.prop("officialHelmChart", cloudUsage.officialHelmChart());
+ json.prop("containerRuntime", cloudUsage.containerRuntime());
json.prop("officialImage", cloudUsage.officialImage());
json.endObject();
}
"kubernetesPlatform": "linux/amd64",
"kubernetesProvider": "5.4.181-99.354.amzn2.x86_64",
"officialHelmChart": "10.1.0",
- "officialImage": false
+ "officialImage": false,
+ "containerRuntime": "docker"
}
}
""");
.setMessageSequenceNumber(1L)
.setPlugins(Collections.emptyMap())
.setManagedInstanceInformation(new TelemetryData.ManagedInstanceInformation(false, null))
- .setCloudUsage(new TelemetryData.CloudUsage(true, "1.27", "linux/amd64", "5.4.181-99.354.amzn2.x86_64", "10.1.0", false))
+ .setCloudUsage(new TelemetryData.CloudUsage(true, "1.27", "linux/amd64", "5.4.181-99.354.amzn2.x86_64", "10.1.0", "docker", false))
.setDatabase(new TelemetryData.Database("H2", "11"))
.setNcdId(NCD_ID);
}
*/
package org.sonar.server.platform;
+import javax.annotation.CheckForNull;
+
public interface ContainerSupport {
/**
* @return {@code true} if we can detect that SQ is running inside a docker container
*/
boolean isRunningInContainer();
+ @CheckForNull
+ String getContainerContext();
+
}
*/
package org.sonar.server.platform;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.util.stream.Stream;
+import com.google.common.annotations.VisibleForTesting;
+import java.util.Objects;
+import java.util.Scanner;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.utils.System2;
import org.sonar.server.util.Paths2;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
public class ContainerSupportImpl implements ContainerSupport {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ContainerSupportImpl.class);
+ private static final String CONTAINER_FILE_PATH = "/run/.containerenv";
+ private static final String DOCKER = "docker";
+ private static final String PODMAN = "podman";
+ private static final String BUILDAH = "buildah";
+ private static final String CONTAINER_D = "containerd";
+ private static final String GENERAL_CONTAINER = "general_container";
+
+ private static final String[] MOUNT_GREP_COMMAND = {"bash", "-c", "mount | grep 'overlay on /'"};
+ private static final String[] CAT_COMMAND = {"bash", "-c", "cat /run/.containerenv"};
+
+ private final System2 system2;
private final Paths2 paths2;
+ private String containerContextCache;
- public ContainerSupportImpl(Paths2 paths2) {
+ public ContainerSupportImpl(Paths2 paths2, System2 system2) {
this.paths2 = paths2;
+ this.system2 = system2;
+
+ populateCache();
+ }
+
+ @VisibleForTesting
+ void populateCache() {
+ if (isDocker()) {
+ containerContextCache = DOCKER;
+ } else if (isPodman()) {
+ containerContextCache = PODMAN;
+ } else if (isBuildah()) {
+ containerContextCache = BUILDAH;
+ } else if (isContainerd()) {
+ containerContextCache = CONTAINER_D;
+ } else if (isGeneralContainer()) {
+ containerContextCache = GENERAL_CONTAINER;
+ } else {
+ containerContextCache = null;
+ }
}
@Override
public boolean isRunningInContainer() {
- if (paths2.exists("/run/.containerenv")) {
- return true;
- }
- try (Stream<String> stream = Files.lines(paths2.get("/proc/1/cgroup"))) {
- return stream.anyMatch(line -> line.contains("/docker") || line.contains("/kubepods") || line.contains("containerd.service") );
- } catch (IOException e) {
- return false;
+ return containerContextCache != null;
+ }
+
+ @Override
+ public String getContainerContext() {
+ return containerContextCache;
+ }
+
+ private boolean isDocker() {
+ return executeCommand(MOUNT_GREP_COMMAND).contains("/docker") && paths2.exists("/.dockerenv");
+ }
+
+ private boolean isPodman() {
+ return Objects.equals(system2.envVariable("container"), PODMAN) && paths2.exists(CONTAINER_FILE_PATH);
+ }
+
+ private boolean isBuildah() {
+ return paths2.exists(CONTAINER_FILE_PATH) && executeCommand(CAT_COMMAND).contains("engine=\"buildah-");
+ }
+
+ private boolean isContainerd() {
+ return executeCommand(MOUNT_GREP_COMMAND).contains("/containerd");
+ }
+
+ private boolean isGeneralContainer() {
+ return paths2.exists(CONTAINER_FILE_PATH);
+ }
+
+ @VisibleForTesting
+ String executeCommand(String[] command) {
+ try {
+ Process process = new ProcessBuilder().command(command).start();
+ try (Scanner scanner = new Scanner(process.getInputStream(), UTF_8)) {
+ scanner.useDelimiter("\n");
+ return scanner.next();
+ } finally {
+ process.destroy();
+ }
+ } catch (Exception e) {
+ LOG.debug("Failed to execute command", e);
+ return "";
}
}
}
import org.slf4j.LoggerFactory;
import org.sonar.api.server.ServerSide;
import org.sonar.api.utils.System2;
+import org.sonar.server.platform.ContainerSupport;
import org.sonar.server.util.Paths2;
import static java.nio.charset.StandardCharsets.UTF_8;
static final String SONAR_HELM_CHART_VERSION = "SONAR_HELM_CHART_VERSION";
static final String DOCKER_RUNNING = "DOCKER_RUNNING";
private static final String[] KUBERNETES_PROVIDER_COMMAND = {"bash", "-c", "uname -r"};
+ private final ContainerSupport containerSupport;
private final System2 system2;
private final Paths2 paths2;
private OkHttpClient httpClient;
private TelemetryData.CloudUsage cloudUsageData;
@Inject
- public CloudUsageDataProvider(System2 system2, Paths2 paths2) {
+ public CloudUsageDataProvider(ContainerSupport containerSupport, System2 system2, Paths2 paths2) {
+ this.containerSupport = containerSupport;
this.system2 = system2;
this.paths2 = paths2;
if (isOnKubernetes()) {
}
@VisibleForTesting
- CloudUsageDataProvider(System2 system2, Paths2 paths2, OkHttpClient httpClient) {
+ CloudUsageDataProvider(ContainerSupport containerSupport, System2 system2, Paths2 paths2, OkHttpClient httpClient) {
+ this.containerSupport = containerSupport;
this.system2 = system2;
this.paths2 = paths2;
this.httpClient = httpClient;
kubernetesPlatform,
getKubernetesProvider(),
getOfficialHelmChartVersion(),
+ containerSupport.getContainerContext(),
isOfficialImageUsed());
return cloudUsageData;
*/
package org.sonar.server.platform;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import org.apache.commons.io.FileUtils;
-import org.junit.Rule;
+import java.util.Arrays;
+import java.util.Collection;
+import org.junit.Assert;
+import org.junit.Before;
import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.sonar.api.utils.System2;
import org.sonar.server.util.Paths2;
-import static java.lang.System.lineSeparator;
-import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
+@RunWith(Parameterized.class)
public class ContainerSupportImplTest {
- private static final String CGROUP_DIR = "/proc/1/cgroup";
- private static final String PODMAN_FILE_PATH = "/run/.containerenv";
-
- @Rule
- public TemporaryFolder temporaryFolder = new TemporaryFolder();
-
- private Paths2 paths2 = mock(Paths2.class);
- private ContainerSupportImpl underTest = new ContainerSupportImpl(paths2);
-
- @Test
- public void isInDocker_returns_false_if_cgroup_file_does_not_exist() throws IOException {
- Path emptyFile = temporaryFolder.newFile().toPath();
- Files.delete(emptyFile);
- when(paths2.get(CGROUP_DIR)).thenReturn(emptyFile);
-
- assertThat(underTest.isRunningInContainer()).isFalse();
+ private static final String CONTAINER_FILE_PATH = "/run/.containerenv";
+ private static final String[] MOUNT_GREP_COMMAND = {"bash", "-c", "mount | grep 'overlay on /'"};
+ private static final String[] CAT_COMMAND = {"bash", "-c", "cat /run/.containerenv"};
+ private static final String DOCKER = "docker";
+ private static final String PODMAN = "podman";
+ private static final String BUILDAH = "buildah";
+ private static final String CONTAINER_D = "containerd";
+ private static final String GENERAL_CONTAINER = "general_container";
+
+ private final Paths2 paths2 = mock(Paths2.class);
+ private final System2 system2 = mock(System2.class);
+ private ContainerSupportImpl underTest = new ContainerSupportImpl(paths2, system2);
+
+ private String containerContext;
+
+ public ContainerSupportImplTest(String containerContext) {
+ this.containerContext = containerContext;
}
- @Test
- public void isInDocker_returns_false_if_cgroup_file_is_empty() throws IOException {
- Path emptyFile = temporaryFolder.newFile().toPath();
- when(paths2.get(CGROUP_DIR)).thenReturn(emptyFile);
-
- assertThat(underTest.isRunningInContainer()).isFalse();
- }
-
- @Test
- public void isInDocker_returns_false_if_cgroup_dir_contains_no_file_with_slash_docker_string() throws IOException {
- Path cgroupFile = temporaryFolder.newFile().toPath();
- String content = "11:name=systemd:/" + lineSeparator() +
- "10:hugetlb:/" + lineSeparator() +
- "9:perf_event:/" + lineSeparator() +
- "8:blkio:/" + lineSeparator() +
- "7:freezer:/" + lineSeparator() +
- "6:devices:/" + lineSeparator() +
- "5:memory:/" + lineSeparator() +
- "4:cpuacct:/" + lineSeparator() +
- "3:cpu:/" + lineSeparator() +
- "2:cpuset:/";
- FileUtils.write(cgroupFile.toFile(), content, StandardCharsets.UTF_8);
- when(paths2.get(CGROUP_DIR)).thenReturn(cgroupFile);
-
- assertThat(underTest.isRunningInContainer()).isFalse();
+ @Before
+ public void setUp() {
+ if (containerContext == null) {
+ return;
+ }
+
+ switch (containerContext) {
+ case DOCKER -> {
+ underTest = spy(underTest);
+ when(underTest.executeCommand(MOUNT_GREP_COMMAND)).thenReturn("/docker");
+ when(paths2.exists("/.dockerenv")).thenReturn(true);
+ }
+ case PODMAN -> {
+ when(system2.envVariable("container")).thenReturn("podman");
+ when(paths2.exists(CONTAINER_FILE_PATH)).thenReturn(true);
+ }
+ case BUILDAH -> {
+ underTest = spy(underTest);
+ when(paths2.exists(CONTAINER_FILE_PATH)).thenReturn(true);
+ when(underTest.executeCommand(CAT_COMMAND)).thenReturn("XXX engine=\"buildah- XXX");
+ }
+ case CONTAINER_D -> {
+ underTest = spy(underTest);
+ when(underTest.executeCommand(MOUNT_GREP_COMMAND)).thenReturn("/containerd");
+ }
+ case GENERAL_CONTAINER -> when(paths2.exists(CONTAINER_FILE_PATH)).thenReturn(true);
+ default -> {
+ }
+ }
+ underTest.populateCache();
}
- @Test
- public void isInDocker_returns_true_if_cgroup_dir_contains_file_with_slash_docker_string() throws IOException {
- Path cgroupFile = temporaryFolder.newFile().toPath();
- String content = "11:name=systemd:/" + lineSeparator() +
- "10:hugetlb:/" + lineSeparator() +
- "9:perf_event:/" + lineSeparator() +
- "8:blkio:/" + lineSeparator() +
- "7:freezer:/" + lineSeparator() +
- "6:devices:/docker/3601745b3bd54d9780436faa5f0e4f72bb46231663bb99a6bb892764917832c2" + lineSeparator() +
- "5:memory:/" + lineSeparator() +
- "4:cpuacct:/" + lineSeparator() +
- "3:cpu:/docker/3601745b3bd54d9780436faa5f0e4f72bb46231663bb99a6bb892764917832c2" + lineSeparator() +
- "2:cpuset:/";
- FileUtils.write(cgroupFile.toFile(), content, StandardCharsets.UTF_8);
- when(paths2.get(CGROUP_DIR)).thenReturn(cgroupFile);
-
- assertThat(underTest.isRunningInContainer()).isTrue();
- }
-
- @Test
- public void isInDocker_returns_true_if_cgroup_dir_contains_file_with_kubepods_string() throws IOException {
- Path cgroupFile = temporaryFolder.newFile().toPath();
- String content = "11:blkio:/kubepods/burstable/pod8e9a7fc0-4e11-4497-a424-19b9713eff0e/8953402928cc7fc95c7dc7bdb75b194139fe29e8fa196d7f90924deb29164366" + lineSeparator() +
- "10:cpuset:/kubepods/burstable/pod8e9a7fc0-4e11-4497-a424-19b9713eff0e/8953402928cc7fc95c7dc7bdb75b194139fe29e8fa196d7f90924deb29164366" + lineSeparator() +
- "9:net_cls,net_prio:/kubepods/burstable/pod8e9a7fc0-4e11-4497-a424-19b9713eff0e/8953402928cc7fc95c7dc7bdb75b194139fe29e8fa196d7f90924deb29164366" + lineSeparator() +
- "8:pids:/kubepods/burstable/pod8e9a7fc0-4e11-4497-a424-19b9713eff0e/8953402928cc7fc95c7dc7bdb75b194139fe29e8fa196d7f90924deb29164366" + lineSeparator() +
- "7:perf_event:/kubepods/burstable/pod8e9a7fc0-4e11-4497-a424-19b9713eff0e/8953402928cc7fc95c7dc7bdb75b194139fe29e8fa196d7f90924deb29164366" + lineSeparator() +
- "6:freezer:/kubepods/burstable/pod8e9a7fc0-4e11-4497-a424-19b9713eff0e/8953402928cc7fc95c7dc7bdb75b194139fe29e8fa196d7f90924deb29164366" + lineSeparator() +
- "5:hugetlb:/kubepods/burstable/pod8e9a7fc0-4e11-4497-a424-19b9713eff0e/8953402928cc7fc95c7dc7bdb75b194139fe29e8fa196d7f90924deb29164366" + lineSeparator() +
- "4:memory:/kubepods/burstable/pod8e9a7fc0-4e11-4497-a424-19b9713eff0e/8953402928cc7fc95c7dc7bdb75b194139fe29e8fa196d7f90924deb29164366" + lineSeparator() +
- "3:devices:/kubepods/burstable/pod8e9a7fc0-4e11-4497-a424-19b9713eff0e/8953402928cc7fc95c7dc7bdb75b194139fe29e8fa196d7f90924deb29164366" + lineSeparator() +
- "2:cpu,cpuacct:/kubepods/burstable/pod8e9a7fc0-4e11-4497-a424-19b9713eff0e/8953402928cc7fc95c7dc7bdb75b194139fe29e8fa196d7f90924deb29164366" + lineSeparator() +
- "1:name=systemd:/kubepods/burstable/pod8e9a7fc0-4e11-4497-a424-19b9713eff0e/8953402928cc7fc95c7dc7bdb75b194139fe29e8fa196d7f90924deb29164366";
- FileUtils.write(cgroupFile.toFile(), content, StandardCharsets.UTF_8);
- when(paths2.get(CGROUP_DIR)).thenReturn(cgroupFile);
-
- assertThat(underTest.isRunningInContainer()).isTrue();
- }
-
- @Test
- public void isInDocker_returns_true_if_cgroup_dir_contains_file_with_containerd_string() throws IOException {
- Path cgroupFile = temporaryFolder.newFile().toPath();
- String content = "12:blkio:/default/846fe494c3021f068c9156ca6eb8a91038389b7e2a2b1ae9b050b33c3a5c9298" + lineSeparator() +
- "11:perf_event:/default/846fe494c3021f068c9156ca6eb8a91038389b7e2a2b1ae9b050b33c3a5c9298" + lineSeparator() +
- "10:hugetlb:/default/846fe494c3021f068c9156ca6eb8a91038389b7e2a2b1ae9b050b33c3a5c9298" + lineSeparator() +
- "9:pids:/default/846fe494c3021f068c9156ca6eb8a91038389b7e2a2b1ae9b050b33c3a5c9298" + lineSeparator() +
- "8:rdma:/" + lineSeparator() +
- "7:memory:/default/846fe494c3021f068c9156ca6eb8a91038389b7e2a2b1ae9b050b33c3a5c9298" + lineSeparator() +
- "6:cpuset:/default/846fe494c3021f068c9156ca6eb8a91038389b7e2a2b1ae9b050b33c3a5c9298" + lineSeparator() +
- "5:net_cls,net_prio:/default/846fe494c3021f068c9156ca6eb8a91038389b7e2a2b1ae9b050b33c3a5c9298" + lineSeparator() +
- "4:freezer:/default/846fe494c3021f068c9156ca6eb8a91038389b7e2a2b1ae9b050b33c3a5c9298" + lineSeparator() +
- "3:cpu,cpuacct:/default/846fe494c3021f068c9156ca6eb8a91038389b7e2a2b1ae9b050b33c3a5c9298" + lineSeparator() +
- "2:devices:/default/846fe494c3021f068c9156ca6eb8a91038389b7e2a2b1ae9b050b33c3a5c9298" + lineSeparator() +
- "1:name=systemd:/default/846fe494c3021f068c9156ca6eb8a91038389b7e2a2b1ae9b050b33c3a5c9298" + lineSeparator() +
- "0::/system.slice/containerd.service";
- FileUtils.write(cgroupFile.toFile(), content, StandardCharsets.UTF_8);
- when(paths2.get(CGROUP_DIR)).thenReturn(cgroupFile);
-
- assertThat(underTest.isRunningInContainer()).isTrue();
+ @Parameterized.Parameters
+ public static Collection<String> data() {
+ return Arrays.asList(DOCKER, PODMAN, BUILDAH, CONTAINER_D, GENERAL_CONTAINER, null);
}
@Test
- public void isInDocker_returns_true_if_podman_file_exists() throws IOException {
- when(paths2.exists(PODMAN_FILE_PATH)).thenReturn(true);
- assertThat(underTest.isRunningInContainer()).isTrue();
+ public void testGetContainerContext() {
+ Assert.assertEquals(containerContext, underTest.getContainerContext());
}
@Test
- public void isInDocker_returns_false_if_podman_file_exists() throws IOException {
- when(paths2.exists(PODMAN_FILE_PATH)).thenReturn(false);
- Path emptyFile = temporaryFolder.newFile().toPath();
- when(paths2.get(CGROUP_DIR)).thenReturn(emptyFile);
- assertThat(underTest.isRunningInContainer()).isFalse();
+ public void testIsRunningInContainer() {
+ boolean expected = containerContext != null;
+ when(paths2.exists(CONTAINER_FILE_PATH)).thenReturn(expected);
+ Assert.assertEquals(expected, underTest.isRunningInContainer());
}
}
import org.junit.Test;
import org.mockito.Mockito;
import org.sonar.api.utils.System2;
+import org.sonar.server.platform.ContainerSupport;
import org.sonar.server.util.Paths2;
import org.sonarqube.ws.MediaTypes;
private final System2 system2 = Mockito.mock(System2.class);
private final Paths2 paths2 = Mockito.mock(Paths2.class);
private final OkHttpClient httpClient = Mockito.mock(OkHttpClient.class);
- private final CloudUsageDataProvider underTest = new CloudUsageDataProvider(system2, paths2, httpClient);
+ private final ContainerSupport containerSupport = mock(ContainerSupport.class);
+ private final CloudUsageDataProvider underTest = new CloudUsageDataProvider(containerSupport, system2, paths2, httpClient);
@Before
public void setUp() throws Exception {
when(httpClient.newCall(any())).thenReturn(callMock);
}
+ @Test
+ public void containerRuntime_whenContainerSupportContextExists_shouldNotBeNull() {
+ when(containerSupport.getContainerContext()).thenReturn("docker");
+ assertThat(underTest.getCloudUsage().containerRuntime()).isEqualTo("docker");
+ }
+
+ @Test
+ public void containerRuntime_whenContainerSupportContextMissing_shouldBeNull() {
+ when(containerSupport.getContainerContext()).thenReturn(null);
+ assertThat(underTest.getCloudUsage().containerRuntime()).isNull();
+ }
+
@Test
public void kubernetes_whenEnvVarExists_shouldReturnTrue() {
assertThat(underTest.getCloudUsage().kubernetes()).isTrue();
public void initHttpClient_whenValidCertificate_shouldCreateClient() throws URISyntaxException {
when(paths2.get(anyString())).thenReturn(Paths.get(requireNonNull(getClass().getResource("dummy.crt")).toURI()));
- CloudUsageDataProvider provider = new CloudUsageDataProvider(system2, paths2);
+ CloudUsageDataProvider provider = new CloudUsageDataProvider(containerSupport, system2, paths2);
assertThat(provider.getHttpClient()).isNotNull();
}
when(paths2.get(anyString())).thenReturn(Paths.get(requireNonNull(getClass().getResource("dummy.crt")).toURI()));
when(system2.envVariable(KUBERNETES_SERVICE_HOST)).thenReturn(null);
- CloudUsageDataProvider provider = new CloudUsageDataProvider(system2, paths2);
+ CloudUsageDataProvider provider = new CloudUsageDataProvider(containerSupport, system2, paths2);
assertThat(provider.getHttpClient()).isNull();
}
public void initHttpClient_whenCertificateNotFound_shouldFail() {
when(paths2.get(any())).thenReturn(Paths.get("dummy.crt"));
- CloudUsageDataProvider provider = new CloudUsageDataProvider(system2, paths2);
+ CloudUsageDataProvider provider = new CloudUsageDataProvider(containerSupport, system2, paths2);
assertThat(provider.getHttpClient()).isNull();
}
}
import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
-import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.HashSet;
@Test
public void load_shouldContainCloudUsage() {
- CloudUsage cloudUsage = new CloudUsage(true, "1.27", "linux/amd64", "5.4.181-99.354.amzn2.x86_64", "10.1.0", false);
+ CloudUsage cloudUsage = new CloudUsage(true, "1.27", "linux/amd64", "5.4.181-99.354.amzn2.x86_64", "10.1.0", "docker", false);
when(cloudUsageDataProvider.getCloudUsage()).thenReturn(cloudUsage);
TelemetryData data = commercialUnderTest.load();