record ManagedInstanceInformation(boolean isManaged, @Nullable String provider) {
}
- record CloudUsage(boolean kubernetes) {
+ record CloudUsage(boolean kubernetes, @Nullable String kubernetesVersion, @Nullable String kubernetesPlatform,
+ @Nullable String kubernetesProvider) {
}
public static class ProjectStatistics {
json.name(CLOUD_USAGE_PROPERTY);
json.beginObject();
json.prop("kubernetes", cloudUsage.kubernetes());
+ json.prop("kubernetesVersion", cloudUsage.kubernetesVersion());
+ json.prop("kubernetesPlatform", cloudUsage.kubernetesPlatform());
+ json.prop("kubernetesProvider", cloudUsage.kubernetesProvider());
json.endObject();
}
}
}
+ @Test
+ public void writeTelemetryData_shouldWriteCloudUsage() {
+ TelemetryData data = telemetryBuilder().build();
+
+ String json = writeTelemetryData(data);
+ assertJson(json).isSimilarTo("""
+ {
+ "cloudUsage": {
+ "kubernetes": true,
+ "kubernetesVersion": "1.27",
+ "kubernetesPlatform": "linux/amd64",
+ "kubernetesProvider": "5.4.181-99.354.amzn2.x86_64"
+ }
+ }
+ """);
+ }
+
@Test
public void writes_has_unanalyzed_languages() {
TelemetryData data = telemetryBuilder()
.setMessageSequenceNumber(1L)
.setPlugins(Collections.emptyMap())
.setManagedInstanceInformation(new TelemetryData.ManagedInstanceInformation(false, null))
- .setCloudUsage(new TelemetryData.CloudUsage(false))
+ .setCloudUsage(new TelemetryData.CloudUsage(true, "1.27", "linux/amd64", "5.4.181-99.354.amzn2.x86_64"))
.setDatabase(new TelemetryData.Database("H2", "11"))
.setNcdId(NCD_ID);
}
*/
package org.sonar.server.telemetry;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gson.Gson;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.SecureRandom;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.util.Collection;
+import java.util.Scanner;
+import javax.annotation.CheckForNull;
+import javax.inject.Inject;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+import okhttp3.internal.tls.OkHostnameVerifier;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.sonar.api.server.ServerSide;
+import org.sonar.api.utils.System2;
+import org.sonar.server.util.Paths2;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.requireNonNull;
@ServerSide
public class CloudUsageDataProvider {
+ private static final Logger LOG = LoggerFactory.getLogger(CloudUsageDataProvider.class);
+
+ private static final String SERVICEACCOUNT_CA_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt";
+ static final String KUBERNETES_SERVICE_HOST = "KUBERNETES_SERVICE_HOST";
+ static final String KUBERNETES_SERVICE_PORT = "KUBERNETES_SERVICE_PORT";
+ private static final String[] KUBERNETES_PROVIDER_COMMAND = {"bash", "-c", "uname -r"};
+ private final System2 system2;
+ private final Paths2 paths2;
+ private OkHttpClient httpClient;
+
+ @Inject
+ public CloudUsageDataProvider(System2 system2, Paths2 paths2) {
+ this.system2 = system2;
+ this.paths2 = paths2;
+ if (isOnKubernetes()) {
+ initHttpClient();
+ }
+ }
+
+ @VisibleForTesting
+ CloudUsageDataProvider(System2 system2, Paths2 paths2, OkHttpClient httpClient) {
+ this.system2 = system2;
+ this.paths2 = paths2;
+ this.httpClient = httpClient;
+ }
+
public TelemetryData.CloudUsage getCloudUsage() {
- return new TelemetryData.CloudUsage(isKubernetes());
+ String kubernetesVersion = null;
+ String kubernetesPlatform = null;
+
+ if (isOnKubernetes()) {
+ VersionInfo versionInfo = getVersionInfo();
+ if (versionInfo != null) {
+ kubernetesVersion = versionInfo.major() + "." + versionInfo.minor();
+ kubernetesPlatform = versionInfo.platform();
+ }
+ }
+
+ return new TelemetryData.CloudUsage(
+ isOnKubernetes(),
+ kubernetesVersion,
+ kubernetesPlatform,
+ getKubernetesProvider());
+ }
+
+ private boolean isOnKubernetes() {
+ return StringUtils.isNotBlank(system2.envVariable(KUBERNETES_SERVICE_HOST));
+ }
+
+ /**
+ * Create a http client to call the Kubernetes API.
+ * This is based on the client creation in the official Kubernetes Java client.
+ */
+ private void initHttpClient() {
+ try {
+ TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init(getKeyStore());
+ TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
+
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, trustManagers, new SecureRandom());
+
+ httpClient = new OkHttpClient.Builder()
+ .sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustManagers[0])
+ .hostnameVerifier(OkHostnameVerifier.INSTANCE)
+ .build();
+ } catch (Exception e) {
+ LOG.debug("Failed to create http client for Kubernetes API", e);
+ }
+ }
+
+ private KeyStore getKeyStore() throws GeneralSecurityException, IOException {
+ KeyStore caKeyStore = newEmptyKeyStore();
+
+ try (FileInputStream fis = new FileInputStream(paths2.get(SERVICEACCOUNT_CA_PATH).toFile())) {
+ CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
+ Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(fis);
+
+ int index = 0;
+ for (Certificate certificate : certificates) {
+ String certificateAlias = "ca" + index;
+ caKeyStore.setCertificateEntry(certificateAlias, certificate);
+ index++;
+ }
+ }
+
+ return caKeyStore;
+ }
+
+ private static KeyStore newEmptyKeyStore() throws GeneralSecurityException, IOException {
+ KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ keyStore.load(null, null);
+ return keyStore;
+ }
+
+ record VersionInfo(String major, String minor, String platform) {
+ }
+
+ private VersionInfo getVersionInfo() {
+ try {
+ Request request = buildRequest();
+ try (Response response = httpClient.newCall(request).execute()) {
+ ResponseBody responseBody = requireNonNull(response.body(), "Response body is null");
+ return new Gson().fromJson(responseBody.string(), VersionInfo.class);
+ }
+ } catch (Exception e) {
+ LOG.debug("Failed to get Kubernetes version info", e);
+ return null;
+ }
+ }
+
+ private Request buildRequest() throws URISyntaxException {
+ String host = system2.envVariable(KUBERNETES_SERVICE_HOST);
+ String port = system2.envVariable(KUBERNETES_SERVICE_PORT);
+ if (host == null || port == null) {
+ throw new IllegalStateException("Kubernetes environment variables are not set");
+ }
+
+ URI uri = new URI("https", null, host, Integer.parseInt(port), "/version", null, null);
+
+ return new Request.Builder()
+ .get()
+ .url(uri.toString())
+ .build();
+ }
+
+ @CheckForNull
+ private static String getKubernetesProvider() {
+ try {
+ Process process = new ProcessBuilder().command(KUBERNETES_PROVIDER_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 get Kubernetes provider", e);
+ return null;
+ }
}
- private static boolean isKubernetes() {
- return true;
+ @VisibleForTesting
+ OkHttpClient getHttpClient() {
+ return httpClient;
}
}
*/
package org.sonar.server.telemetry;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.file.Paths;
+import javax.annotation.Nullable;
+import okhttp3.Call;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Protocol;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+import org.junit.Before;
import org.junit.Test;
+import org.mockito.Mockito;
+import org.sonar.api.utils.System2;
+import org.sonar.server.util.Paths2;
+import org.sonarqube.ws.MediaTypes;
+import static java.util.Objects.requireNonNull;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.telemetry.CloudUsageDataProvider.KUBERNETES_SERVICE_HOST;
+import static org.sonar.server.telemetry.CloudUsageDataProvider.KUBERNETES_SERVICE_PORT;
public class CloudUsageDataProviderTest {
- private final CloudUsageDataProvider underTest = new CloudUsageDataProvider();
+ 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);
+
+ @Before
+ public void setUp() throws Exception {
+ when(system2.envVariable(KUBERNETES_SERVICE_HOST)).thenReturn("localhost");
+ when(system2.envVariable(KUBERNETES_SERVICE_PORT)).thenReturn("443");
+
+ mockHttpClientCall(200, "OK", ResponseBody.create("""
+ {
+ "major": "1",
+ "minor": "25",
+ "gitVersion": "v1.25.3",
+ "gitCommit": "434bfd82814af038ad94d62ebe59b133fcb50506",
+ "gitTreeState": "clean",
+ "buildDate": "2022-11-02T03:24:50Z",
+ "goVersion": "go1.19.2",
+ "compiler": "gc",
+ "platform": "linux/arm64"
+ }
+ """, MediaType.parse(MediaTypes.JSON)));
+ }
+
+ private void mockHttpClientCall(int code, String message, @Nullable ResponseBody body) throws IOException {
+ Call callMock = mock(Call.class);
+ when(callMock.execute()).thenReturn(new Response.Builder()
+ .request(new Request.Builder().url("http://any.test/").build())
+ .protocol(Protocol.HTTP_1_1)
+ .code(code)
+ .message(message)
+ .body(body)
+ .build());
+ when(httpClient.newCall(any())).thenReturn(callMock);
+ }
@Test
- public void isKubernetes_shouldReturnValue() {
+ public void kubernetes_whenEnvVarExists_shouldReturnTrue() {
assertThat(underTest.getCloudUsage().kubernetes()).isTrue();
}
+
+ @Test
+ public void kubernetes_whenEnvVarDoesNotExist_shouldReturnFalse() {
+ when(system2.envVariable(KUBERNETES_SERVICE_HOST)).thenReturn(null);
+ assertThat(underTest.getCloudUsage().kubernetes()).isFalse();
+ }
+
+ @Test
+ public void kubernetesVersion_whenOnKubernetes_shouldReturnValue() {
+ assertThat(underTest.getCloudUsage().kubernetesVersion()).isEqualTo("1.25");
+ }
+
+ @Test
+ public void kubernetesVersion_whenNotOnKubernetes_shouldReturnNull() {
+ when(system2.envVariable(KUBERNETES_SERVICE_HOST)).thenReturn(null);
+ assertThat(underTest.getCloudUsage().kubernetesVersion()).isNull();
+ }
+
+ @Test
+ public void kubernetesVersion_whenApiCallFails_shouldReturnNull() throws IOException {
+ mockHttpClientCall(404, "not found", null);
+ assertThat(underTest.getCloudUsage().kubernetesVersion()).isNull();
+ }
+
+ @Test
+ public void kubernetesPlatform_whenOnKubernetes_shouldReturnValue() {
+ assertThat(underTest.getCloudUsage().kubernetesPlatform()).isEqualTo("linux/arm64");
+ }
+
+ @Test
+ public void kubernetesPlatform_whenNotOnKubernetes_shouldReturnNull() {
+ when(system2.envVariable(KUBERNETES_SERVICE_HOST)).thenReturn(null);
+ assertThat(underTest.getCloudUsage().kubernetesPlatform()).isNull();
+ }
+
+ @Test
+ public void kubernetesPlatform_whenApiCallFails_shouldReturnNull() throws IOException {
+ mockHttpClientCall(404, "not found", null);
+ assertThat(underTest.getCloudUsage().kubernetesPlatform()).isNull();
+ }
+
+ @Test
+ public void kubernetesProvider_shouldReturnValue() {
+ assertThat(underTest.getCloudUsage().kubernetesProvider()).isNotBlank();
+ }
+
+ @Test
+ 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);
+ assertThat(provider.getHttpClient()).isNotNull();
+ }
+
+ @Test
+ public void initHttpClient_whenNotOnKubernetes_shouldNotCreateClient() throws URISyntaxException {
+ 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);
+ assertThat(provider.getHttpClient()).isNull();
+ }
+
+ @Test
+ public void initHttpClient_whenCertificateNotFound_shouldFail() {
+ when(paths2.get(any())).thenReturn(Paths.get("dummy.crt"));
+
+ CloudUsageDataProvider provider = new CloudUsageDataProvider(system2, paths2);
+ assertThat(provider.getHttpClient()).isNull();
+ }
}
assertThat(managedInstance.provider()).isEqualTo(provider);
}
+ @Test
+ public void load_shouldContainCloudUsage() {
+ TelemetryData.CloudUsage cloudUsage = new TelemetryData.CloudUsage(true, "1.27", "linux/amd64", "5.4.181-99.354.amzn2.x86_64");
+ when(cloudUsageDataProvider.getCloudUsage()).thenReturn(cloudUsage);
+
+ TelemetryData data = commercialUnderTest.load();
+ assertThat(data.getCloudUsage()).isEqualTo(cloudUsage);
+ }
+
@Test
public void default_quality_gate_sent_with_project() {
db.components().insertPublicProject().getMainBranchComponent();
--- /dev/null
+-----BEGIN CERTIFICATE-----
+MIICQjCCAaugAwIBAgIBADANBgkqhkiG9w0BAQ0FADA9MQswCQYDVQQGEwJ1czEO
+MAwGA1UECAwFZHVtbXkxDjAMBgNVBAoMBWR1bW15MQ4wDAYDVQQDDAVkdW1teTAg
+Fw0yMzA2MDkxMDMxMzRaGA8yMjk3MDMyNDEwMzEzNFowPTELMAkGA1UEBhMCdXMx
+DjAMBgNVBAgMBWR1bW15MQ4wDAYDVQQKDAVkdW1teTEOMAwGA1UEAwwFZHVtbXkw
+gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAPL0Byqouz9UNBFRLqRRuNdGniwh
+LzheMFKsdQIasTddfbsne6IuqMIBRyNr/icPrxXZEx/LY7mlKpBCYM/yty5ngEon
+0QTTw/GXj3A7eDcpYD/0pVRKFcKNFIp58IKV09to2h4ttQUdjMqLS2yjc0ADugmy
+ctlTR90Yna31Gi/nAgMBAAGjUDBOMB0GA1UdDgQWBBSMaHVg1zegjAH8CEZdN87I
+FtN/6jAfBgNVHSMEGDAWgBSMaHVg1zegjAH8CEZdN87IFtN/6jAMBgNVHRMEBTAD
+AQH/MA0GCSqGSIb3DQEBDQUAA4GBAFRViPwyPMBY6auUmaywjeLqtVPfn58MNssN
+TZEh4ft3d2Z531m5thtSiZhnKFU/f1xRecUXK3jew8/RAKVSsTH7A4NYfhu5Bs/K
+JfFWv7NYwL5ntnaBQZQ5uSYwPwiTYZzFrTgEDDOkXpsf7g5A16hS/L11A1lwx9b4
+WWCrjmv4
+-----END CERTIFICATE-----