diff --git a/spring-cloud-kubernetes-client-autoconfig/pom.xml b/spring-cloud-kubernetes-client-autoconfig/pom.xml index eaf5d38f..37861fcf 100644 --- a/spring-cloud-kubernetes-client-autoconfig/pom.xml +++ b/spring-cloud-kubernetes-client-autoconfig/pom.xml @@ -48,6 +48,11 @@ spring-boot-starter-web test + + org.mockito + mockito-inline + test + diff --git a/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientPodUtils.java b/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientPodUtils.java index c0ace643..15370170 100644 --- a/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientPodUtils.java +++ b/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientPodUtils.java @@ -25,8 +25,10 @@ import io.kubernetes.client.util.Config; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.cloud.kubernetes.commons.EnvReader; import org.springframework.cloud.kubernetes.commons.LazilyInstantiate; import org.springframework.cloud.kubernetes.commons.PodUtils; +import org.springframework.util.StringUtils; /** * @author Ryan Baxter @@ -38,15 +40,22 @@ public class KubernetesClientPodUtils implements PodUtils { */ public static final String HOSTNAME = "HOSTNAME"; + /** + * KUBERNETES_SERVICE_HOST environment variable name. + */ + public static final String KUBERNETES_SERVICE_HOST = "KUBERNETES_SERVICE_HOST"; + private static final Log LOG = LogFactory.getLog(KubernetesClientPodUtils.class); private final CoreV1Api client; private final String hostName; - private Supplier current; + private final Supplier current; - private String namespace; + private final String namespace; + + private final String serviceHost; public KubernetesClientPodUtils(CoreV1Api client, String namespace) { if (client == null) { @@ -54,14 +63,15 @@ public class KubernetesClientPodUtils implements PodUtils { } this.client = client; - this.hostName = System.getenv(HOSTNAME); - this.current = LazilyInstantiate.using(() -> internalGetPod()); + this.hostName = EnvReader.getEnv(HOSTNAME); + this.serviceHost = EnvReader.getEnv(KUBERNETES_SERVICE_HOST); + this.current = LazilyInstantiate.using(this::internalGetPod); this.namespace = namespace; } @Override public Supplier currentPod() { - return this.current; + return current; } @Override @@ -69,30 +79,35 @@ public class KubernetesClientPodUtils implements PodUtils { return currentPod().get() != null; } - private synchronized V1Pod internalGetPod() { + private V1Pod internalGetPod() { try { - LOG.info("Getting pod internal"); - if (isServiceAccountFound() && isHostNameEnvVarPresent()) { - return this.client.readNamespacedPod(this.hostName, namespace, null, null, null); - } - else { - return null; + if (isServiceHostEnvVarPresent() && isHostNameEnvVarPresent() && isServiceAccountFound()) { + return client.readNamespacedPod(hostName, namespace, null, null, null); } } catch (Throwable t) { - LOG.warn("Failed to get pod with name:[" + this.hostName + "]. You should look into this if things aren't" + LOG.warn("Failed to get pod with name:[" + hostName + "]. You should look into this if things aren't" + " working as you expect. Are you missing serviceaccount permissions?", t); - return null; } + return null; + } + + private boolean isServiceHostEnvVarPresent() { + return StringUtils.hasLength(serviceHost); } private boolean isHostNameEnvVarPresent() { - return this.hostName != null && !this.hostName.isEmpty(); + return StringUtils.hasLength(hostName); } private boolean isServiceAccountFound() { - return Paths.get(Config.SERVICEACCOUNT_TOKEN_PATH).toFile().exists() - && Paths.get(Config.SERVICEACCOUNT_CA_PATH).toFile().exists(); + boolean serviceAccountPathPresent = Paths.get(Config.SERVICEACCOUNT_TOKEN_PATH).toFile().exists(); + if (!serviceAccountPathPresent) { + // https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/ + LOG.warn("serviceaccount path not present, did you disable it via 'automountServiceAccountToken : false'?" + + " Major functionalities will not work without that property being set"); + } + return serviceAccountPathPresent && Paths.get(Config.SERVICEACCOUNT_CA_PATH).toFile().exists(); } } diff --git a/spring-cloud-kubernetes-client-autoconfig/src/test/java/org/springframework/cloud/kubernetes/client/KubernetesClientPodUtilsTests.java b/spring-cloud-kubernetes-client-autoconfig/src/test/java/org/springframework/cloud/kubernetes/client/KubernetesClientPodUtilsTests.java new file mode 100644 index 00000000..1a866507 --- /dev/null +++ b/spring-cloud-kubernetes-client-autoconfig/src/test/java/org/springframework/cloud/kubernetes/client/KubernetesClientPodUtilsTests.java @@ -0,0 +1,174 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.function.Supplier; + +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1Pod; +import io.kubernetes.client.util.Config; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import org.springframework.cloud.kubernetes.commons.EnvReader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * @author wind57 + */ +public class KubernetesClientPodUtilsTests { + + private static final String KUBERNETES_SERVICE_HOST = KubernetesClientPodUtils.KUBERNETES_SERVICE_HOST; + + private static final String HOSTNAME = KubernetesClientPodUtils.HOSTNAME; + + private static final String SERVICE_ACCOUNT_TOKEN_PATH = Config.SERVICEACCOUNT_TOKEN_PATH; + + private static final String SERVICE_ACCOUNT_CERT_PATH = Config.SERVICEACCOUNT_CA_PATH; + + private static final String POD_HOSTNAME = "pod-hostname"; + + private static final String HOST = "10.1.1.1"; + + private static final V1Pod POD = new V1Pod(); + + private final CoreV1Api client = Mockito.mock(CoreV1Api.class); + + private final Path tokenPath = Mockito.mock(Path.class); + + private final File tokenFile = Mockito.mock(File.class); + + private final Path certPath = Mockito.mock(Path.class); + + private final File certFile = Mockito.mock(File.class); + + private MockedStatic envReader; + + private MockedStatic paths; + + @BeforeEach + public void before() { + envReader = Mockito.mockStatic(EnvReader.class); + paths = Mockito.mockStatic(Paths.class); + } + + @AfterEach + public void after() { + envReader.close(); + paths.close(); + } + + @Test + public void constructorThrowsIllegalArgumentExceptionWhenKubeClientIsNull() { + assertThatThrownBy(() -> new KubernetesClientPodUtils(null, "namespace")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Must provide an instance of KubernetesClient"); + } + + @Test + public void serviceHostNotPresent() { + mockHost(null); + + KubernetesClientPodUtils util = new KubernetesClientPodUtils(client, "namespace"); + Supplier sup = util.currentPod(); + assertSupplierAndClient(sup, util); + } + + @Test + public void hostNameNotPresent() { + mockHost(HOST); + mockHostname(null); + + KubernetesClientPodUtils util = new KubernetesClientPodUtils(client, "namespace"); + Supplier sup = util.currentPod(); + assertSupplierAndClient(sup, util); + } + + @Test + public void serviceAccountPathNotPresent() { + mockTokenPath(false); + mockHostname(HOST); + + KubernetesClientPodUtils util = new KubernetesClientPodUtils(client, "namespace"); + Supplier sup = util.currentPod(); + assertSupplierAndClient(sup, util); + } + + @Test + public void serviceAccountCertPathNotPresent() { + mockTokenPath(true); + mockCertPath(false); + mockHostname(HOST); + + KubernetesClientPodUtils util = new KubernetesClientPodUtils(client, "namespace"); + Supplier sup = util.currentPod(); + assertSupplierAndClient(sup, util); + } + + @Test + public void allPresent() throws ApiException { + mockTokenPath(true); + mockCertPath(true); + mockHost(HOST); + mockHostname(POD_HOSTNAME); + mockPodResult(); + + KubernetesClientPodUtils util = new KubernetesClientPodUtils(client, "namespace"); + Supplier sup = util.currentPod(); + assertThat(sup.get()).isNotNull(); + assertThat(util.isInsideKubernetes()).isTrue(); + } + + private void assertSupplierAndClient(Supplier sup, KubernetesClientPodUtils util) { + assertThat(sup.get()).isNull(); + assertThat(util.isInsideKubernetes()).isFalse(); + } + + private void mockHost(String host) { + envReader.when(() -> EnvReader.getEnv(KUBERNETES_SERVICE_HOST)).thenReturn(host); + } + + private void mockHostname(String name) { + envReader.when(() -> EnvReader.getEnv(HOSTNAME)).thenReturn(name); + } + + private void mockTokenPath(boolean result) { + Mockito.when(tokenPath.toFile()).thenReturn(tokenFile); + Mockito.when(tokenFile.exists()).thenReturn(result); + paths.when(() -> Paths.get(SERVICE_ACCOUNT_TOKEN_PATH)).thenReturn(tokenPath); + } + + private void mockCertPath(boolean result) { + Mockito.when(certPath.toFile()).thenReturn(certFile); + Mockito.when(certFile.exists()).thenReturn(result); + paths.when(() -> Paths.get(SERVICE_ACCOUNT_CERT_PATH)).thenReturn(certPath); + } + + private void mockPodResult() throws ApiException { + Mockito.when(client.readNamespacedPod(POD_HOSTNAME, "namespace", null, null, null)).thenReturn(POD); + } + +}