diff --git a/spring-cloud-kubernetes-commons/pom.xml b/spring-cloud-kubernetes-commons/pom.xml index 2d24d052..b3cda1b4 100644 --- a/spring-cloud-kubernetes-commons/pom.xml +++ b/spring-cloud-kubernetes-commons/pom.xml @@ -50,6 +50,12 @@ org.springframework.boot spring-boot-starter-test + + + org.mockito + mockito-core + + test @@ -57,6 +63,17 @@ junit-vintage-engine test + + + org.mockito + mockito-inline + test + + + org.springframework + spring-test + test + diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/AbstractKubernetesHealthIndicator.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/AbstractKubernetesHealthIndicator.java index 7f72f4a8..b2fd0151 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/AbstractKubernetesHealthIndicator.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/AbstractKubernetesHealthIndicator.java @@ -67,7 +67,7 @@ public abstract class AbstractKubernetesHealthIndicator extends AbstractHealthIn public static final String LABELS = "labels"; @Override - protected void doHealthCheck(Health.Builder builder) throws Exception { + protected void doHealthCheck(Health.Builder builder) { try { builder.up().withDetails(getDetails()); } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/profile/AbstractKubernetesProfileEnvironmentPostProcessor.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/profile/AbstractKubernetesProfileEnvironmentPostProcessor.java index 2ecd76b1..1d14bc8a 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/profile/AbstractKubernetesProfileEnvironmentPostProcessor.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/profile/AbstractKubernetesProfileEnvironmentPostProcessor.java @@ -16,19 +16,17 @@ package org.springframework.cloud.kubernetes.commons.profile; -import java.io.File; import java.io.IOException; import java.nio.file.Files; -import java.nio.file.LinkOption; -import java.util.HashMap; -import java.util.Map; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; import org.springframework.boot.SpringApplication; import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor; import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.boot.logging.DeferredLog; import org.springframework.core.Ordered; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; @@ -41,7 +39,7 @@ import static org.springframework.cloud.kubernetes.commons.KubernetesClientPrope */ public abstract class AbstractKubernetesProfileEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { - private static final Log LOG = LogFactory.getLog(AbstractKubernetesProfileEnvironmentPostProcessor.class); + private static final DeferredLog LOG = new DeferredLog(); private static final String NAMESPACE_PATH_PROPERTY = "spring.cloud.kubernetes.client.serviceAccountNamespacePath"; @@ -60,41 +58,20 @@ public abstract class AbstractKubernetesProfileEnvironmentPostProcessor implemen @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { - final boolean kubernetesEnabled = environment.getProperty("spring.cloud.kubernetes.enabled", Boolean.class, - true); + application.addInitializers(ctx -> LOG.replayTo(AbstractKubernetesProfileEnvironmentPostProcessor.class)); + + boolean kubernetesEnabled = environment.getProperty("spring.cloud.kubernetes.enabled", Boolean.class, true); if (!kubernetesEnabled) { return; } addNamespaceFromServiceAccountFile(environment); - if (isInsideKubernetes(environment)) { - if (hasKubernetesProfile(environment)) { - if (LOG.isDebugEnabled()) { - LOG.debug("'kubernetes' already in list of active profiles"); - } - } - else { - if (LOG.isDebugEnabled()) { - LOG.debug("Adding 'kubernetes' to list of active profiles"); - } - environment.addActiveProfile(KUBERNETES_PROFILE); - } - } - else { - if (LOG.isDebugEnabled()) { - LOG.warn("Not running inside kubernetes. Skipping 'kubernetes' profile activation."); - } - } + addKubernetesProfileIfMissing(environment); } protected abstract boolean isInsideKubernetes(Environment environment); private boolean hasKubernetesProfile(Environment environment) { - for (String activeProfile : environment.getActiveProfiles()) { - if (KUBERNETES_PROFILE.equalsIgnoreCase(activeProfile)) { - return true; - } - } - return false; + return Arrays.stream(environment.getActiveProfiles()).anyMatch(KUBERNETES_PROFILE::equalsIgnoreCase); } @Override @@ -102,30 +79,43 @@ public abstract class AbstractKubernetesProfileEnvironmentPostProcessor implemen return ORDER; } - private void addNamespaceFromServiceAccountFile(ConfigurableEnvironment environment) { - Map properties = new HashMap<>(); + private void addKubernetesProfileIfMissing(ConfigurableEnvironment environment) { + if (isInsideKubernetes(environment)) { + if (hasKubernetesProfile(environment)) { + LOG.debug("'kubernetes' already in list of active profiles"); + } + else { + LOG.debug("Adding 'kubernetes' to list of active profiles"); + environment.addActiveProfile(KUBERNETES_PROFILE); + } + } + else { + LOG.warn("Not running inside kubernetes. Skipping 'kubernetes' profile activation."); + } + } + private void addNamespaceFromServiceAccountFile(ConfigurableEnvironment environment) { String serviceAccountNamespace = environment.getProperty(NAMESPACE_PATH_PROPERTY, SERVICE_ACCOUNT_NAMESPACE_PATH); - LOG.debug("Looking for service account namespace at " + serviceAccountNamespace); - boolean serviceAccountNamespaceExists = Files.isRegularFile((new File(serviceAccountNamespace)).toPath(), - new LinkOption[0]); + LOG.debug("Looking for service account namespace at: [" + serviceAccountNamespace + "]."); + Path serviceAccountNamespacePath = Paths.get(serviceAccountNamespace); + boolean serviceAccountNamespaceExists = Files.isRegularFile(serviceAccountNamespacePath); if (serviceAccountNamespaceExists) { LOG.debug("Found service account namespace at: [" + serviceAccountNamespace + "]."); try { - String namespace = new String(Files.readAllBytes((new File(serviceAccountNamespace)).toPath())); + String namespace = new String(Files.readAllBytes((serviceAccountNamespacePath))); LOG.debug("Service account namespace value: " + namespace); - properties.put(NAMESPACE_PROPERTY, namespace); + environment.getPropertySources().addLast(new MapPropertySource(PROPERTY_SOURCE_NAME, + Collections.singletonMap(NAMESPACE_PROPERTY, namespace))); } - catch (IOException var4) { - LOG.error("Error reading service account namespace from: [" + serviceAccountNamespace + "].", var4); + catch (IOException ioe) { + LOG.error("Error reading service account namespace from: [" + serviceAccountNamespace + "].", ioe); } } else { LOG.info("Did not find service account namespace at: [" + serviceAccountNamespace + "]. Ignoring."); } - environment.getPropertySources().addLast(new MapPropertySource(PROPERTY_SOURCE_NAME, properties)); } } diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/profile/AbstractKubernetesProfileEnvironmentPostProcessorTest.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/profile/AbstractKubernetesProfileEnvironmentPostProcessorTest.java new file mode 100644 index 00000000..8ab4106b --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/profile/AbstractKubernetesProfileEnvironmentPostProcessorTest.java @@ -0,0 +1,248 @@ +/* + * 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.commons.profile; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Optional; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertySource; +import org.springframework.test.context.support.TestPropertySourceUtils; + +/** + * @author wind57 + */ +public class AbstractKubernetesProfileEnvironmentPostProcessorTest { + + private static final String FOUNT_IT = "foundIt"; + + private static final String PATH = "/some/path"; + + private MockedStatic paths; + + private MockedStatic files; + + private final SpringApplication springApplication = Mockito.mock(SpringApplication.class); + + private final ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(); + + private static final AbstractKubernetesProfileEnvironmentPostProcessor POST_PROCESSOR_INSIDE = new AbstractKubernetesProfileEnvironmentPostProcessor() { + @Override + protected boolean isInsideKubernetes(Environment environment) { + return true; + } + }; + + private static final AbstractKubernetesProfileEnvironmentPostProcessor POST_PROCESSOR_OUTSIDE = new AbstractKubernetesProfileEnvironmentPostProcessor() { + @Override + protected boolean isInsideKubernetes(Environment environment) { + return false; + } + }; + + @Before + public void before() { + paths = Mockito.mockStatic(Paths.class); + files = Mockito.mockStatic(Files.class); + } + + @After + public void after() { + paths.close(); + files.close(); + } + + /** + *
+	 * 1) "spring.cloud.kubernetes.enabled" is false; thus nothing happens
+	 * 
+ */ + @Test + public void testKubernetesDisabled() { + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(context, "spring.cloud.kubernetes.enabled=false"); + POST_PROCESSOR_INSIDE.postProcessEnvironment(context.getEnvironment(), springApplication); + + assertKubernetesProfileNotPresent(); + assertKubernetesPropertySourceNotPresent(); + + } + + /** + *
+	 * 1) "spring.cloud.kubernetes.enabled" is true
+	 * 2) "spring.cloud.kubernetes.client.serviceAccountNamespacePath" is present, but does not resolve to an actual File
+	 * 
+ */ + @Test + public void testKubernetesEnabledAndServiceAccountNamespacePathIsNotResolved() { + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(context, "spring.cloud.kubernetes.enabled=true", + "spring.cloud.kubernetes.client.serviceAccountNamespacePath=" + PATH); + serviceAccountFileResolved(false, PATH); + POST_PROCESSOR_INSIDE.postProcessEnvironment(context.getEnvironment(), springApplication); + + assertKubernetesProfilePresent(); + assertKubernetesPropertySourceNotPresent(); + + } + + /** + *
+	 * 1) "spring.cloud.kubernetes.enabled" is true
+	 * 2) "spring.cloud.kubernetes.client.serviceAccountNamespacePath" is present and resolves to an actual File
+	 * 
+ */ + @Test + public void testKubernetesEnabledAndServiceAccountNamespacePathIsResolved() { + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(context, "spring.cloud.kubernetes.enabled=true", + "spring.cloud.kubernetes.client.serviceAccountNamespacePath=" + PATH); + + Path path = serviceAccountFileResolved(true, PATH); + mockServiceAccountNamespace(path); + + POST_PROCESSOR_INSIDE.postProcessEnvironment(context.getEnvironment(), springApplication); + + assertKubernetesProfilePresent(); + assertKubernetesPropertySourcePresent(); + + } + + /** + *
+	 * 1) "spring.cloud.kubernetes.enabled" is true
+	 * 2) "spring.cloud.kubernetes.client.serviceAccountNamespacePath" is not present, as such:
+	 * 3) "/var/run/secrets/kubernetes.io/serviceaccount/namespace" is picked up, which is resolved and present
+	 * 
+ */ + @Test + public void testKubernetesEnabledAndServiceAccountNamespacePathIsResolvedViaDefaultLocation() { + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(context, "spring.cloud.kubernetes.enabled=true"); + + Path path = serviceAccountFileResolved(true, "/var/run/secrets/kubernetes.io/serviceaccount/namespace"); + mockServiceAccountNamespace(path); + + POST_PROCESSOR_INSIDE.postProcessEnvironment(context.getEnvironment(), springApplication); + + assertKubernetesProfilePresent(); + assertKubernetesPropertySourcePresent(); + } + + /** + *
+	 * 1) "spring.cloud.kubernetes.enabled" is true
+	 * 2) isInsideKubernetes returns false
+	 * 3) "spring.cloud.kubernetes.client.serviceAccountNamespacePath" is not present, as such:
+	 * 4) "/var/run/secrets/kubernetes.io/serviceaccount/namespace" is picked up, which is resolved and present
+	 * 
+ */ + @Test + public void testOutsideKubernetes() { + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(context, "spring.cloud.kubernetes.enabled=true"); + + Path path = serviceAccountFileResolved(true, "/var/run/secrets/kubernetes.io/serviceaccount/namespace"); + mockServiceAccountNamespace(path); + + POST_PROCESSOR_OUTSIDE.postProcessEnvironment(context.getEnvironment(), springApplication); + + assertKubernetesProfileNotPresent(); + assertKubernetesPropertySourcePresent(); + } + + /* + * 'kubernetes' profile is not present + */ + private void assertKubernetesProfileNotPresent() { + Assert.assertFalse("'kubernetes' profile must not be present when 'spring.cloud.kubernetes.enabled' is false", + kubernetesProfile().isPresent()); + } + + /* + * 'kubernetes' profile is present + */ + private void assertKubernetesProfilePresent() { + Assert.assertTrue("'kubernetes' profile must be present when 'spring.cloud.kubernetes.enabled' is true", + kubernetesProfile().isPresent()); + } + + /* + * 'KUBERNETES_NAMESPACE_PROPERTY_SOURCE' source is not present + */ + private void assertKubernetesPropertySourceNotPresent() { + Optional> kubernetesPropertySource = kubernetesPropertySource(); + + Assert.assertFalse( + "'KUBERNETES_NAMESPACE_PROPERTY_SOURCE' source must not be present when 'spring.cloud.kubernetes.enabled' is false", + kubernetesPropertySource.isPresent()); + } + + /* + * 'KUBERNETES_NAMESPACE_PROPERTY_SOURCE' source is present + */ + private void assertKubernetesPropertySourcePresent() { + + Optional> kubernetesPropertySource = kubernetesPropertySource(); + Assert.assertTrue( + "'KUBERNETES_NAMESPACE_PROPERTY_SOURCE' source must be present when 'spring.cloud.kubernetes.enabled' is true", + kubernetesPropertySource.isPresent()); + + String property = (String) kubernetesPropertySource.get() + .getProperty("spring.cloud.kubernetes.client.namespace"); + Assert.assertEquals("'spring.cloud.kubernetes.client.namespace' must be set to 'foundIt'", property, FOUNT_IT); + } + + /** + *
+	 * 1) serviceAccountNamespace File is present or not
+	 * 2) if the above is present, under what actualPath
+	 * 
+ */ + private Path serviceAccountFileResolved(boolean present, String actualPath) { + Path path = Mockito.mock(Path.class); + paths.when(() -> Paths.get(actualPath)).thenReturn(path); + files.when(() -> Files.isRegularFile(path)).thenReturn(present); + return path; + } + + /* + * returns "foundIt" for service account namespace + */ + private void mockServiceAccountNamespace(Path path) { + files.when(() -> Files.readAllBytes(path)).thenReturn(FOUNT_IT.getBytes()); + } + + private Optional kubernetesProfile() { + return Arrays.stream(context.getEnvironment().getActiveProfiles()).filter("kubernetes"::equals).findFirst(); + } + + private Optional> kubernetesPropertySource() { + return context.getEnvironment().getPropertySources().stream() + .filter(x -> "KUBERNETES_NAMESPACE_PROPERTY_SOURCE".equals(x.getName())).findAny(); + } + +}