Merge branch '2.0.x' into main

This commit is contained in:
Ryan Baxter
2021-09-15 17:15:39 -04:00
19 changed files with 573 additions and 40 deletions

View File

@@ -46,6 +46,9 @@ spring:
In the preceding example, if `spring.cloud.kubernetes.config.namespace` had not been set,
the `ConfigMap` named `c1` would be looked up in the namespace that the application runs.
See <<namespace-resolution,Namespace resolution>> to get a better understanding of how the namespace
of the application is resolved.
Any matching `ConfigMap` that is found is processed as follows:
@@ -562,7 +565,7 @@ spring:
sources:
# Spring Cloud Kubernetes looks up a Secret named s1 in namespace default-namespace
- name: s1
# Spring Cloud Kubernetes looks up a Secret named default-name in whatever namespace n2
# Spring Cloud Kubernetes looks up a Secret named default-name in namespace n2
- namespace: n2
# Spring Cloud Kubernetes looks up a Secret named s3 in namespace n3
- namespace: n3
@@ -572,6 +575,8 @@ spring:
In the preceding example, if `spring.cloud.kubernetes.secrets.namespace` had not been set,
the `Secret` named `s1` would be looked up in the namespace that the application runs.
See <<namespace-resolution,namespace-resolution>> to get a better understanding of how the namespace
of the application is resolved.
.Properties:
@@ -597,6 +602,44 @@ https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-Configuration-Bi
You can find an example of an application that uses secrets (though it has not been updated to use the new `spring-cloud-kubernetes` project) at
https://github.com/fabric8-quickstarts/spring-boot-camel-config[spring-boot-camel-config]
[[namespace-resolution]]
=== Namespace resolution
Finding an application namespace happens on a best-effort basis. There are some steps that we iterate in order
to find it. The easiest and most common one, is to specify it in the proper configuration, for example:
====
[source,yaml]
----
spring:
application:
name: app
cloud:
kubernetes:
secrets:
name: secret
namespace: default
sources:
# Spring Cloud Kubernetes looks up a Secret named 'a' in namespace 'default'
- name: a
# Spring Cloud Kubernetes looks up a Secret named 'secret' in namespace 'b'
- namespace: b
# Spring Cloud Kubernetes looks up a Secret named 'd' in namespace 'c'
- namespace: c
name: d
----
====
Remember that the same can be done for config maps. If such a namespace is not specified, it will be read (in this order):
1. from property `spring.cloud.kubernetes.client.namespace`
2. from a String residing in a file denoted by `spring.cloud.kubernetes.client.serviceAccountNamespacePath` property
3. from a String residing in `/var/run/secrets/kubernetes.io/serviceaccount/namespace` file
(kubernetes default namespace path)
4. from a designated client method call (for example fabric8's : `KubernetesClient::getNamespace`), if the client provides
such a method.
Failure to find a namespace from the above steps will result in an Exception being raised.
=== `PropertySource` Reload
WARNING: This functionality has been deprecated in the 2020.0 release. Please see

View File

@@ -74,6 +74,12 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>

View File

@@ -22,10 +22,10 @@ import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties;
import org.springframework.cloud.kubernetes.commons.config.ConfigMapPropertySourceLocator;
import org.springframework.cloud.kubernetes.commons.config.NamespaceResolutionFailedException;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import static org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigUtils.getNamespace;
import org.springframework.util.StringUtils;
/**
* @author Ryan Baxter
@@ -38,6 +38,14 @@ public class KubernetesClientConfigMapPropertySourceLocator extends ConfigMapPro
private final KubernetesNamespaceProvider kubernetesNamespaceProvider;
/**
* This constructor is deprecated. Its usage might cause unexpected behavior when
* looking for different properties. For example, in general, if a namespace is not
* provided, we might look it up via other means: different documented environment
* variables or from a kubernetes client itself. Using this constructor might not
* reflect that.
*/
@Deprecated
public KubernetesClientConfigMapPropertySourceLocator(CoreV1Api coreV1Api, ConfigMapConfigProperties properties,
KubernetesClientProperties kubernetesClientProperties) {
super(properties);
@@ -59,10 +67,28 @@ public class KubernetesClientConfigMapPropertySourceLocator extends ConfigMapPro
ConfigMapConfigProperties.NormalizedSource normalizedSource, String configurationTarget,
ConfigurableEnvironment environment) {
String fallbackNamespace = kubernetesNamespaceProvider != null ? kubernetesNamespaceProvider.getNamespace()
: kubernetesClientProperties.getNamespace();
return new KubernetesClientConfigMapPropertySource(coreV1Api, name,
getNamespace(normalizedSource, fallbackNamespace), environment, normalizedSource.getPrefix());
String namespace;
String normalizedNamespace = normalizedSource.getNamespace();
if (StringUtils.hasText(normalizedNamespace)) {
namespace = normalizedNamespace;
}
else if (kubernetesClientProperties != null) {
if (StringUtils.hasText(kubernetesClientProperties.getNamespace())) {
namespace = kubernetesClientProperties.getNamespace();
}
else {
throw new NamespaceResolutionFailedException(
"could not resolve namespace in normalized source or KubernetesClientProperties");
}
}
else {
namespace = KubernetesClientConfigUtils.getApplicationNamespace(normalizedNamespace, "Config Map",
kubernetesNamespaceProvider);
}
return new KubernetesClientConfigMapPropertySource(coreV1Api, name, namespace, environment,
normalizedSource.getPrefix());
}
}

View File

@@ -16,8 +16,13 @@
package org.springframework.cloud.kubernetes.client.config;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties;
import org.springframework.cloud.kubernetes.commons.config.NamespaceResolutionFailedException;
import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties;
import org.springframework.util.StringUtils;
@@ -26,6 +31,8 @@ import org.springframework.util.StringUtils;
*/
public final class KubernetesClientConfigUtils {
private static final Log LOG = LogFactory.getLog(KubernetesClientConfigUtils.class);
private KubernetesClientConfigUtils() {
}
@@ -51,16 +58,56 @@ public final class KubernetesClientConfigUtils {
}
}
@Deprecated
public static String getNamespace(ConfigMapConfigProperties.NormalizedSource normalizedSource,
String fallbackNamespace) {
String normalizedNamespace = normalizedSource.getNamespace();
return StringUtils.hasText(normalizedNamespace) ? normalizedNamespace : fallbackNamespace;
}
@Deprecated
public static String getNamespace(SecretsConfigProperties.NormalizedSource normalizedSource,
String fallbackNamespace) {
String normalizedNamespace = normalizedSource.getNamespace();
return StringUtils.hasText(normalizedNamespace) ? normalizedNamespace : fallbackNamespace;
}
/**
* this method does the namespace resolution for both config map and secrets
* implementations. It tries these places to find the namespace:
*
* <pre>
* 1. from a normalized source (which can be null)
* 2. from a property 'spring.cloud.kubernetes.client.namespace', if such is present
* 3. from a String residing in a file denoted by `spring.cloud.kubernetes.client.serviceAccountNamespacePath`
* property, if such is present
* 4. from a String residing in `/var/run/secrets/kubernetes.io/serviceaccount/namespace` file,
* if such is present (kubernetes default path)
* </pre>
*
* If any of the above fail, we throw a NamespaceResolutionFailedException.
* @param namespace normalized namespace
* @param configurationTarget Config Map/Secret
* @param provider the provider which computes the namespace
* @return application namespace
* @throws NamespaceResolutionFailedException when namespace could not be resolved
*/
static String getApplicationNamespace(String namespace, String configurationTarget,
KubernetesNamespaceProvider provider) {
if (StringUtils.hasText(namespace)) {
LOG.debug(configurationTarget + " namespace from normalized source : " + namespace);
return namespace;
}
if (provider != null) {
String providerNamespace = provider.getNamespace();
if (StringUtils.hasText(providerNamespace)) {
LOG.debug(configurationTarget + " namespace from provider : " + namespace);
return providerNamespace;
}
}
throw new NamespaceResolutionFailedException("unresolved namespace");
}
}

View File

@@ -20,12 +20,13 @@ import io.kubernetes.client.openapi.apis.CoreV1Api;
import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.config.NamespaceResolutionFailedException;
import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties;
import org.springframework.cloud.kubernetes.commons.config.SecretsPropertySourceLocator;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.util.StringUtils;
import static org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigUtils.getNamespace;
import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.getApplicationName;
/**
@@ -66,11 +67,30 @@ public class KubernetesClientSecretsPropertySourceLocator extends SecretsPropert
@Override
protected MapPropertySource getPropertySource(ConfigurableEnvironment environment,
SecretsConfigProperties.NormalizedSource normalizedSource, String configurationTarget) {
String fallbackNamespace = kubernetesNamespaceProvider != null ? kubernetesNamespaceProvider.getNamespace()
: kubernetesClientProperties.getNamespace();
return new KubernetesClientSecretsPropertySource(coreV1Api,
getApplicationName(environment, normalizedSource.getName(), configurationTarget),
getNamespace(normalizedSource, fallbackNamespace), environment, normalizedSource.getLabels());
String namespace;
String normalizedNamespace = normalizedSource.getNamespace();
String secretName = getApplicationName(environment, normalizedSource.getName(), configurationTarget);
if (StringUtils.hasText(normalizedNamespace)) {
namespace = normalizedNamespace;
}
else if (kubernetesClientProperties != null) {
if (StringUtils.hasText(kubernetesClientProperties.getNamespace())) {
namespace = kubernetesClientProperties.getNamespace();
}
else {
throw new NamespaceResolutionFailedException(
"could not resolve namespace in normalized source or KubernetesClientProperties");
}
}
else {
namespace = KubernetesClientConfigUtils.getApplicationNamespace(normalizedNamespace, "Secret",
kubernetesNamespaceProvider);
}
return new KubernetesClientSecretsPropertySource(coreV1Api, secretName, namespace, environment,
normalizedSource.getLabels());
}
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright 2013-2020 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.config;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ConfigurableApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author wind57
*/
public class KubernetesClientBootstrapConfigurationTests {
@SpringBootApplication
static class Application {
}
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = Application.class,
properties = "spring.cloud.kubernetes.enabled=false")
@Nested
class KubernetesDisabled {
@Autowired
ConfigurableApplicationContext context;
@Test
void configAndSecretsBeansAreNotPresent() {
assertThat(context.getBeanNamesForType(KubernetesClientConfigMapPropertySourceLocator.class)).hasSize(0);
assertThat(context.getBeanNamesForType(KubernetesClientSecretsPropertySourceLocator.class)).hasSize(0);
}
}
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = Application.class,
properties = { "spring.cloud.kubernetes.secrets.enabled=true",
"spring.cloud.kubernetes.client.namespace=default" })
@Nested
class KubernetesEnabledOnPurpose {
@Autowired
ConfigurableApplicationContext context;
@Test
void configAndSecretsBeansArePresent() {
assertThat(context.getBeanNamesForType(KubernetesClientConfigMapPropertySourceLocator.class)).hasSize(1);
assertThat(context.getBeanNamesForType(KubernetesClientSecretsPropertySourceLocator.class)).hasSize(1);
}
}
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = Application.class,
properties = "spring.cloud.kubernetes.client.namespace=default")
@Nested
class KubernetesEnabled {
@Autowired
ConfigurableApplicationContext context;
@Test
void configAndSecretsBeansArePresent() {
assertThat(context.getBeanNamesForType(KubernetesClientConfigMapPropertySourceLocator.class)).hasSize(1);
assertThat(context.getBeanNamesForType(KubernetesClientSecretsPropertySourceLocator.class)).hasSize(1);
}
}
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = Application.class,
properties = "spring.cloud.kubernetes.config.enabled=false")
@Nested
class KubernetesEnabledConfigDisabled {
@Autowired
ConfigurableApplicationContext context;
@Test
void secretsOnlyPresent() {
assertThat(context.getBeanNamesForType(KubernetesClientConfigMapPropertySourceLocator.class)).hasSize(0);
assertThat(context.getBeanNamesForType(KubernetesClientSecretsPropertySourceLocator.class)).hasSize(1);
}
}
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = Application.class,
properties = { "spring.cloud.kubernetes.secrets.enabled=false",
"spring.cloud.kubernetes.client.namespace=default" })
@Nested
class KubernetesEnabledSecretsDisabled {
@Autowired
ConfigurableApplicationContext context;
@Test
void secretsOnlyPresent() {
assertThat(context.getBeanNamesForType(KubernetesClientConfigMapPropertySourceLocator.class)).hasSize(1);
assertThat(context.getBeanNamesForType(KubernetesClientSecretsPropertySourceLocator.class)).hasSize(0);
}
}
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = Application.class,
properties = { "spring.cloud.kubernetes.secrets.enabled=false",
"spring.cloud.kubernetes.config.enabled=false" })
@Nested
class KubernetesEnabledSecretsAndConfigDisabled {
@Autowired
ConfigurableApplicationContext context;
@Test
void secretsOnlyPresent() {
assertThat(context.getBeanNamesForType(KubernetesClientConfigMapPropertySourceLocator.class)).hasSize(0);
assertThat(context.getBeanNamesForType(KubernetesClientSecretsPropertySourceLocator.class)).hasSize(0);
}
}
}

View File

@@ -35,7 +35,9 @@ import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties;
import org.springframework.cloud.kubernetes.commons.config.NamespaceResolutionFailedException;
import org.springframework.core.env.PropertySource;
import org.springframework.mock.env.MockEnvironment;
@@ -44,6 +46,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* @author Ryan Baxter
@@ -121,4 +124,48 @@ class KubernetesClientConfigMapPropertySourceLocatorTests {
.isTrue();
}
/**
* <pre>
* 1. using the deprecated constructor, and
* 2. not providing the namespace
* </pre>
*
* will result in an Exception
*/
@Test
void testLocateWithoutNamespaceDeprecatedConstructor() {
CoreV1Api api = new CoreV1Api();
stubFor(get(API)
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(PROPERTIES_CONFIGMAP_LIST))));
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties();
configMapConfigProperties.setName("bootstrap-640");
KubernetesClientProperties kubernetesClientProperties = new KubernetesClientProperties();
kubernetesClientProperties.setNamespace(""); // empty on purpose
assertThatThrownBy(() -> new KubernetesClientConfigMapPropertySourceLocator(api, configMapConfigProperties,
kubernetesClientProperties).locate(new MockEnvironment()))
.isInstanceOf(NamespaceResolutionFailedException.class);
}
/**
* <pre>
* 1. using the non-deprecated constructor, and
* 2. not providing the namespace
* </pre>
*
* will result in an Exception
*/
@Test
void testLocateWithoutNamespace() {
CoreV1Api api = new CoreV1Api();
stubFor(get(API)
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(PROPERTIES_CONFIGMAP_LIST))));
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties();
configMapConfigProperties.setName("bootstrap-640");
KubernetesClientProperties kubernetesClientProperties = new KubernetesClientProperties();
kubernetesClientProperties.setNamespace(""); // empty on purpose
assertThatThrownBy(() -> new KubernetesClientConfigMapPropertySourceLocator(api, configMapConfigProperties,
new KubernetesNamespaceProvider(new MockEnvironment())).locate(new MockEnvironment()))
.isInstanceOf(NamespaceResolutionFailedException.class);
}
}

View File

@@ -0,0 +1,54 @@
/*
* 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.config;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.config.NamespaceResolutionFailedException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* @author wind57
*/
class KubernetesClientConfigUtilsTests {
private final KubernetesNamespaceProvider provider = Mockito.mock(KubernetesNamespaceProvider.class);
@Test
void testNamespaceFromNormalizedSource() {
String result = KubernetesClientConfigUtils.getApplicationNamespace("abc", "target", null);
assertThat(result).isEqualTo("abc");
}
@Test
void testNamespaceFromProvider() {
Mockito.when(provider.getNamespace()).thenReturn("def");
String result = KubernetesClientConfigUtils.getApplicationNamespace("", "target", provider);
assertThat(result).isEqualTo("def");
}
@Test
void testNamespaceResolutionFailed() {
assertThatThrownBy(() -> KubernetesClientConfigUtils.getApplicationNamespace("", "target", null))
.isInstanceOf(NamespaceResolutionFailedException.class);
}
}

View File

@@ -31,6 +31,8 @@ import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.config.NamespaceResolutionFailedException;
import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties;
import org.springframework.core.env.PropertySource;
import org.springframework.mock.env.MockEnvironment;
@@ -40,15 +42,14 @@ import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* @author Ryan Baxter
*/
class KubernetesClientSecretsPropertySourceLocatorTests {
private static final String LIST_API = "/api/v1/secrets";
private static final String LIST_API_WITH_LABEL = "/api/v1/secrets?labelSelector=spring.cloud.kubernetes.secret%3Dtrue";
private static final String LIST_API = "/api/v1/namespaces/default/secrets";
private static final String LIST_BODY = "{\n" + "\t\"kind\": \"SecretList\",\n" + "\t\"apiVersion\": \"v1\",\n"
+ "\t\"metadata\": {\n" + "\t\t\"selfLink\": \"/api/v1/secrets\",\n"
@@ -112,10 +113,10 @@ class KubernetesClientSecretsPropertySourceLocatorTests {
sources.add(source1);
sources.add(source2);
secretsConfigProperties.setName("app");
secretsConfigProperties.setNamespace("");
secretsConfigProperties.setNamespace("default");
secretsConfigProperties.setSources(sources);
secretsConfigProperties.setEnableApi(true);
PropertySource propertySource = new KubernetesClientSecretsPropertySourceLocator(api,
PropertySource<?> propertySource = new KubernetesClientSecretsPropertySourceLocator(api,
new KubernetesClientProperties(), secretsConfigProperties).locate(new MockEnvironment());
assertThat(propertySource.containsProperty("password")).isTrue();
assertThat(propertySource.getProperty("password")).isEqualTo("p455w0rd");
@@ -127,12 +128,54 @@ class KubernetesClientSecretsPropertySourceLocatorTests {
stubFor(get(LIST_API).willReturn(aResponse().withStatus(200).withBody(LIST_BODY)));
SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties();
secretsConfigProperties.setName("db-secret");
secretsConfigProperties.setNamespace("");
secretsConfigProperties.setNamespace("default");
secretsConfigProperties.setEnableApi(true);
PropertySource propertySource = new KubernetesClientSecretsPropertySourceLocator(api,
PropertySource<?> propertySource = new KubernetesClientSecretsPropertySourceLocator(api,
new KubernetesClientProperties(), secretsConfigProperties).locate(new MockEnvironment());
assertThat(propertySource.containsProperty("password")).isTrue();
assertThat(propertySource.getProperty("password")).isEqualTo("p455w0rd");
}
/**
* <pre>
* 1. using the deprecated constructor, and
* 2. not providing the namespace
* </pre>
*
* will result in an Exception
*/
@Test
void testLocateWithoutNamespaceDeprecatedConstructor() {
CoreV1Api api = new CoreV1Api();
stubFor(get(LIST_API).willReturn(aResponse().withStatus(200).withBody(LIST_BODY)));
SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties();
secretsConfigProperties.setName("db-secret");
secretsConfigProperties.setNamespace(""); // empty on purpose
secretsConfigProperties.setEnableApi(true);
assertThatThrownBy(() -> new KubernetesClientSecretsPropertySourceLocator(api, new KubernetesClientProperties(),
secretsConfigProperties).locate(new MockEnvironment()))
.isInstanceOf(NamespaceResolutionFailedException.class);
}
/**
* <pre>
* 1. using the non-deprecated constructor, and
* 2. not providing the namespace
* </pre>
*
* will result in an Exception
*/
@Test
void testLocateWithoutNamespace() {
CoreV1Api api = new CoreV1Api();
stubFor(get(LIST_API).willReturn(aResponse().withStatus(200).withBody(LIST_BODY)));
SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties();
secretsConfigProperties.setName("db-secret");
secretsConfigProperties.setNamespace(""); // empty on purpose
secretsConfigProperties.setEnableApi(true);
assertThatThrownBy(() -> new KubernetesClientSecretsPropertySourceLocator(api,
new KubernetesNamespaceProvider(new MockEnvironment()), secretsConfigProperties)
.locate(new MockEnvironment())).isInstanceOf(NamespaceResolutionFailedException.class);
}
}

View File

@@ -0,0 +1,32 @@
/*
* 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.config;
/**
* @author wind57
*
* Thrown when a namespace could not be resolved. Resolution of a namespace happens via
* user provided input, environment properties or directly from the underlying client,
* etc.
*/
public class NamespaceResolutionFailedException extends RuntimeException {
public NamespaceResolutionFailedException(String message) {
super(message);
}
}

View File

@@ -29,14 +29,12 @@ import org.springframework.core.env.MapPropertySource;
*/
public class SecretsPropertySource extends MapPropertySource {
private static final String PREFIX = "secrets";
public SecretsPropertySource(String name, Map<String, Object> source) {
super(name, source);
}
protected static String getSourceName(String name, String namespace) {
return PREFIX + Constants.PROPERTY_SOURCE_NAME_SEPARATOR + name + Constants.PROPERTY_SOURCE_NAME_SEPARATOR
return "secrets" + Constants.PROPERTY_SOURCE_NAME_SEPARATOR + name + Constants.PROPERTY_SOURCE_NAME_SEPARATOR
+ namespace;
}

View File

@@ -47,6 +47,11 @@ public class Fabric8ConfigMapPropertySource extends ConfigMapPropertySource {
this(client, name, null, null, "");
}
/**
* this constructor is present only for compatibility reasons, its usage is
* discouraged.
*/
@Deprecated
public Fabric8ConfigMapPropertySource(KubernetesClient client, String applicationName, String namespace,
Environment environment) {
super(getName(applicationName, getApplicationNamespace(client, namespace)),

View File

@@ -66,9 +66,9 @@ public class Fabric8ConfigMapPropertySourceLocator extends ConfigMapPropertySour
@Override
protected MapPropertySource getMapPropertySource(String applicationName, NormalizedSource normalizedSource,
String configurationTarget, ConfigurableEnvironment environment) {
String configMapName = getApplicationNamespace(this.client, normalizedSource.getNamespace(),
configurationTarget, provider);
return new Fabric8ConfigMapPropertySource(this.client, applicationName, configMapName, environment,
String namespace = getApplicationNamespace(this.client, normalizedSource.getNamespace(), configurationTarget,
provider);
return new Fabric8ConfigMapPropertySource(this.client, applicationName, namespace, environment,
normalizedSource.getPrefix());
}

View File

@@ -25,6 +25,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.config.NamespaceResolutionFailedException;
import org.springframework.util.StringUtils;
/**
@@ -39,6 +40,9 @@ public final class Fabric8ConfigUtils {
private Fabric8ConfigUtils() {
}
/*
* this is not used, it is here for compatibility reasons only.
*/
@Deprecated
public static String getApplicationNamespace(KubernetesClient client, String namespace,
String configurationTarget) {
@@ -51,6 +55,28 @@ public final class Fabric8ConfigUtils {
return namespace;
}
/**
* this method does the namespace resolution for both config map and secrets
* implementations. It tries these places to find the namespace:
*
* <pre>
* 1. from a normalized source (which can be null)
* 2. from a property 'spring.cloud.kubernetes.client.namespace', if such is present
* 3. from a String residing in a file denoted by `spring.cloud.kubernetes.client.serviceAccountNamespacePath`
* property, if such is present
* 4. from a String residing in `/var/run/secrets/kubernetes.io/serviceaccount/namespace` file,
* if such is present (kubernetes default path)
* 5. from KubernetesClient::getNamespace, which is implementation specific.
* </pre>
*
* If any of the above fail, we throw a NamespaceResolutionFailedException.
* @param namespace normalized namespace
* @param configurationTarget Config Map/Secret
* @param provider the provider which computes the namespace
* @param client fabric8 Kubernetes client
* @return application namespace
* @throws NamespaceResolutionFailedException when namespace could not be resolved
*/
static String getApplicationNamespace(KubernetesClient client, String namespace, String configurationTarget,
KubernetesNamespaceProvider provider) {
@@ -63,11 +89,16 @@ public final class Fabric8ConfigUtils {
String providerNamespace = provider.getNamespace();
if (StringUtils.hasText(providerNamespace)) {
LOG.debug(configurationTarget + " namespace from provider : " + namespace);
return providerNamespace;
}
}
LOG.debug(configurationTarget + " namespace from client : " + client.getNamespace());
return client.getNamespace();
String clientNamespace = client.getNamespace();
LOG.debug(configurationTarget + " namespace from client : " + clientNamespace);
if (clientNamespace == null) {
throw new NamespaceResolutionFailedException("unresolved namespace");
}
return clientNamespace;
}
@@ -75,7 +106,7 @@ public final class Fabric8ConfigUtils {
return !StringUtils.hasLength(namespace) ? client.getNamespace() : namespace;
}
public static Map<String, String> getConfigMapData(KubernetesClient client, String namespace, String name) {
static Map<String, String> getConfigMapData(KubernetesClient client, String namespace, String name) {
ConfigMap configMap = !StringUtils.hasLength(namespace) ? client.configMaps().withName(name).get()
: client.configMaps().inNamespace(namespace).withName(name).get();

View File

@@ -39,7 +39,8 @@ public class Fabric8ActuatorTests {
@Nested
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = App.class, properties = {
"management.health.kubernetes.enabled=false", "management.endpoint.health.show-details=always",
"management.endpoint.health.show-components=always", "management.endpoints.web.exposure.include=health" })
"management.endpoint.health.show-components=always", "management.endpoints.web.exposure.include=health",
"spring.cloud.kubernetes.client.namespace=default" })
public class DisabledHealthTest {
@Autowired
@@ -66,7 +67,8 @@ public class Fabric8ActuatorTests {
@Nested
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = App.class, properties = {
"management.health.kubernetes.enabled=true", "management.endpoint.health.show-details=always",
"management.endpoint.health.show-components=always", "management.endpoints.web.exposure.include=health" })
"management.endpoint.health.show-components=always", "management.endpoints.web.exposure.include=health",
"spring.cloud.kubernetes.client.namespace=default" })
public class EnabledHealthTest {
@Autowired

View File

@@ -47,7 +47,8 @@ public class Fabric8BootstrapConfigurationTests {
}
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = Application.class,
properties = "spring.cloud.kubernetes.enabled=true")
properties = { "spring.cloud.kubernetes.secrets.enabled=true",
"spring.cloud.kubernetes.client.namespace=default" })
@Nested
class KubernetesEnabledOnPurpose {
@@ -62,7 +63,8 @@ public class Fabric8BootstrapConfigurationTests {
}
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = Application.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = Application.class,
properties = "spring.cloud.kubernetes.client.namespace=default")
@Nested
class KubernetesEnabled {
@@ -94,7 +96,8 @@ public class Fabric8BootstrapConfigurationTests {
}
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = Application.class,
properties = "spring.cloud.kubernetes.secrets.enabled=false")
properties = { "spring.cloud.kubernetes.secrets.enabled=false",
"spring.cloud.kubernetes.client.namespace=default" })
@Nested
class KubernetesEnabledSecretsDisabled {

View File

@@ -16,11 +16,17 @@
package org.springframework.cloud.kubernetes.fabric8.config;
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.config.NamespaceResolutionFailedException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* @author wind57
@@ -30,6 +36,10 @@ public class Fabric8ConfigUtilsTests {
private KubernetesClient client;
private final DefaultKubernetesClient mockClient = Mockito.mock(DefaultKubernetesClient.class);
private final KubernetesNamespaceProvider provider = Mockito.mock(KubernetesNamespaceProvider.class);
@Test
public void testGetApplicationNamespaceNotPresent() {
String result = Fabric8ConfigUtils.getApplicationNamespace(client, "", "target");
@@ -42,4 +52,30 @@ public class Fabric8ConfigUtilsTests {
assertThat(result).isEqualTo("namespace");
}
@Test
void testNamespaceFromNormalizedSource() {
String result = Fabric8ConfigUtils.getApplicationNamespace(client, "abc", "target", null);
assertThat(result).isEqualTo("abc");
}
@Test
void testNamespaceFromProvider() {
Mockito.when(provider.getNamespace()).thenReturn("def");
String result = Fabric8ConfigUtils.getApplicationNamespace(client, "", "target", provider);
assertThat(result).isEqualTo("def");
}
@Test
void testNamespaceFromClient() {
Mockito.when(mockClient.getNamespace()).thenReturn("qwe");
String result = Fabric8ConfigUtils.getApplicationNamespace(mockClient, "", "target", null);
assertThat(result).isEqualTo("qwe");
}
@Test
void testNamespaceResolutionFailed() {
assertThatThrownBy(() -> Fabric8ConfigUtils.getApplicationNamespace(mockClient, "", "target", null))
.isInstanceOf(NamespaceResolutionFailedException.class);
}
}

View File

@@ -42,7 +42,8 @@ public class KubernetesConfigConfigurationTest extends KubernetesConfigTestBase
@Test
public void kubernetesWhenKubernetesDefaultEnabled() {
setup(KubernetesClientTestConfiguration.class, "spring.cloud.kubernetes.enabled=true");
setup(KubernetesClientTestConfiguration.class, "spring.cloud.kubernetes.enabled=true",
"spring.cloud.kubernetes.client.namespace=default");
assertThat(getContext().containsBean("configMapPropertySourceLocator")).isTrue();
assertThat(getContext().containsBean("secretsPropertySourceLocator")).isTrue();
}
@@ -65,7 +66,7 @@ public class KubernetesConfigConfigurationTest extends KubernetesConfigTestBase
@Test
public void kubernetesWhenKubernetesConfigEnabledButSecretDisabled() {
setup(KubernetesClientTestConfiguration.class, "spring.cloud.kubernetes.config.enabled=true",
"spring.cloud.kubernetes.secrets.enabled=false");
"spring.cloud.kubernetes.secrets.enabled=false", "spring.cloud.kubernetes.client.namespace=default");
assertThat(getContext().containsBean("configMapPropertySourceLocator")).isTrue();
assertThat(getContext().containsBean("secretsPropertySourceLocator")).isFalse();
}
@@ -81,7 +82,7 @@ public class KubernetesConfigConfigurationTest extends KubernetesConfigTestBase
@Test
public void kubernetesConfigWhenKubernetesEnabledAndKubernetesConfigEnabled() {
setup(KubernetesClientTestConfiguration.class, "spring.cloud.kubernetes.config.enabled=true",
"spring.cloud.kubernetes.secrets.enabled=true");
"spring.cloud.kubernetes.secrets.enabled=true", "spring.cloud.kubernetes.client.namespace=default");
assertThat(getContext().containsBean("configMapPropertySourceLocator")).isTrue();
assertThat(getContext().containsBean("secretsPropertySourceLocator")).isTrue();
}

View File

@@ -37,13 +37,14 @@ import static org.assertj.core.api.Assertions.assertThat;
@ClassPathExclusions({ "spring-boot-actuator-autoconfigure-*.jar", "spring-boot-starter-actuator-*.jar" })
public class MissingActuatorTest {
private static ConfigurableApplicationContext getApplicationContext(Class<?> configuration, String... properties) {
return new SpringApplicationBuilder(configuration).web(WebApplicationType.NONE).properties(properties).run();
private static ConfigurableApplicationContext getApplicationContext(String... properties) {
return new SpringApplicationBuilder(Config.class).web(WebApplicationType.NONE).properties(properties).run();
}
@Test
public void unknownClassProtected(CapturedOutput capturedOutput) {
try (ConfigurableApplicationContext context = getApplicationContext(Config.class, "debug=true")) {
try (ConfigurableApplicationContext context = getApplicationContext("debug=true",
"spring.cloud.kubernetes.client.namespace=default")) {
String output = capturedOutput.toString();
assertThat(output)
.doesNotContain("Failed to introspect annotations on"