Support Kubernetes config fail-fast and retry (#873)

* Add support for fail fast loading ConfigMaps and Secrets

* Add support for retry loading ConfigMaps and Secrets on failure
This commit is contained in:
Işık Erhan
2021-10-26 16:46:16 +03:00
committed by GitHub
parent e7d864daf7
commit d338511332
41 changed files with 2398 additions and 66 deletions

View File

@@ -459,15 +459,37 @@ NOTE: If you use `spring.cloud.kubernetes.config.paths` or `spring.cloud.kubern
functionality will not work. You will need to make a `POST` request to the `/actuator/refresh` endpoint or
restart/redeploy the application.
[#config-map-fail-fast]
In some cases, your application may be unable to load some of your `ConfigMaps` using the Kubernetes API.
If you want your application to fail the start-up process in such cases, you can set
`spring.cloud.kubernetes.config.fail-fast=true` to make the application start-up fail with an Exception.
[#config-map-retry]
You can also make your application retry loading `ConfigMap` property sources on a failure. First, you need to
set `spring.cloud.kubernetes.config.fail-fast=true`. Then you need to add `spring-retry`
and `spring-boot-starter-aop` to your classpath. You can configure retry properties such as
the maximum number of attempts, backoff options like initial interval, multiplier, max interval by setting the
`spring.cloud.kubernetes.config.retry.*` properties.
NOTE: If you already have `spring-retry` and `spring-boot-starter-aop` on the classpath for some reason
and want to enable fail-fast, but do not want retry to be enabled; you can disable retry for `ConfigMap` `PropertySources`
by setting `spring.cloud.kubernetes.config.retry.enabled=false`.
.Properties:
[options="header,footer"]
|===
| Name | Type | Default | Description
| `spring.cloud.kubernetes.config.enabled` | `Boolean` | `true` | Enable ConfigMaps `PropertySource`
| `spring.cloud.kubernetes.config.name` | `String` | `${spring.application.name}` | Sets the name of `ConfigMap` to look up
| `spring.cloud.kubernetes.config.namespace` | `String` | Client namespace | Sets the Kubernetes namespace where to lookup
| `spring.cloud.kubernetes.config.paths` | `List` | `null` | Sets the paths where `ConfigMap` instances are mounted
| `spring.cloud.kubernetes.config.enableApi` | `Boolean` | `true` | Enable or disable consuming `ConfigMap` instances through APIs
| Name | Type | Default | Description
| `spring.cloud.kubernetes.config.enabled` | `Boolean` | `true` | Enable ConfigMaps `PropertySource`
| `spring.cloud.kubernetes.config.name` | `String` | `${spring.application.name}` | Sets the name of `ConfigMap` to look up
| `spring.cloud.kubernetes.config.namespace` | `String` | Client namespace | Sets the Kubernetes namespace where to lookup
| `spring.cloud.kubernetes.config.paths` | `List` | `null` | Sets the paths where `ConfigMap` instances are mounted
| `spring.cloud.kubernetes.config.enableApi` | `Boolean` | `true` | Enable or disable consuming `ConfigMap` instances through APIs
| `spring.cloud.kubernetes.config.fail-fast` | `Boolean` | `false` | Enable or disable failing the application start-up when an error occurred while loading a `ConfigMap`
| `spring.cloud.kubernetes.config.retry.enabled` | `Boolean` | `true` | Enable or disable config retry.
| `spring.cloud.kubernetes.config.retry.initial-interval` | `Long` | `1000` | Initial retry interval in milliseconds.
| `spring.cloud.kubernetes.config.retry.max-attempts` | `Integer` | `6` | Maximum number of attempts.
| `spring.cloud.kubernetes.config.retry.max-interval` | `Long` | `2000` | Maximum interval for backoff.
| `spring.cloud.kubernetes.config.retry.multiplier` | `Double` | `1.1` | Multiplier for next interval.
|===
=== Secrets PropertySource
@@ -622,17 +644,35 @@ the `Secret` named `s1` would be looked up in the namespace that the application
See <<namespace-resolution,namespace-resolution>> to get a better understanding of how the namespace
of the application is resolved.
<<config-map-fail-fast,Similar to the `ConfigMaps`>>; if you want your application to fail to start
when it is unable to load `Secrets` property sources, you can set `spring.cloud.kubernetes.secrets.fail-fast=true`.
It is also possible to enable retry for `Secret` property sources <<config-map-retry,like the `ConfigMaps`>>.
As with the `ConfigMap` property sources, first you need to set `spring.cloud.kubernetes.secrets.fail-fast=true`.
Then you need to add `spring-retry` and `spring-boot-starter-aop` to your classpath.
Retry behavior of the `Secret` property sources can be configured by setting the `spring.cloud.kubernetes.secrets.retry.*`
properties.
NOTE: If you already have `spring-retry` and `spring-boot-starter-aop` on the classpath for some reason
and want to enable fail-fast, but do not want retry to be enabled; you can disable retry for `Secrets` `PropertySources`
by setting `spring.cloud.kubernetes.secrets.retry.enabled=false`.
.Properties:
[options="header,footer"]
|===
| Name | Type | Default | Description
| `spring.cloud.kubernetes.secrets.enabled` | `Boolean` | `true` | Enable Secrets `PropertySource`
| `spring.cloud.kubernetes.secrets.name` | `String` | `${spring.application.name}` | Sets the name of the secret to look up
| `spring.cloud.kubernetes.secrets.namespace` | `String` | Client namespace | Sets the Kubernetes namespace where to look up
| `spring.cloud.kubernetes.secrets.labels` | `Map` | `null` | Sets the labels used to lookup secrets
| `spring.cloud.kubernetes.secrets.paths` | `List` | `null` | Sets the paths where secrets are mounted (example 1)
| `spring.cloud.kubernetes.secrets.enableApi` | `Boolean` | `false` | Enables or disables consuming secrets through APIs (examples 2 and 3)
| Name | Type | Default | Description
| `spring.cloud.kubernetes.secrets.enabled` | `Boolean` | `true` | Enable Secrets `PropertySource`
| `spring.cloud.kubernetes.secrets.name` | `String` | `${spring.application.name}` | Sets the name of the secret to look up
| `spring.cloud.kubernetes.secrets.namespace` | `String` | Client namespace | Sets the Kubernetes namespace where to look up
| `spring.cloud.kubernetes.secrets.labels` | `Map` | `null` | Sets the labels used to lookup secrets
| `spring.cloud.kubernetes.secrets.paths` | `List` | `null` | Sets the paths where secrets are mounted (example 1)
| `spring.cloud.kubernetes.secrets.enableApi` | `Boolean` | `false` | Enables or disables consuming secrets through APIs (examples 2 and 3)
| `spring.cloud.kubernetes.secrets.fail-fast` | `Boolean` | `false` | Enable or disable failing the application start-up when an error occurred while loading a `Secret`
| `spring.cloud.kubernetes.secrets.retry.enabled` | `Boolean` | `true` | Enable or disable secrets retry.
| `spring.cloud.kubernetes.secrets.retry.initial-interval` | `Long` | `1000` | Initial retry interval in milliseconds.
| `spring.cloud.kubernetes.secrets.retry.max-attempts` | `Integer` | `6` | Maximum number of attempts.
| `spring.cloud.kubernetes.secrets.retry.max-interval` | `Long` | `2000` | Maximum interval for backoff.
| `spring.cloud.kubernetes.secrets.retry.multiplier` | `Double` | `1.1` | Multiplier for next interval.
|===
Notes:

View File

@@ -99,6 +99,21 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2020 the original author or authors.
* 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.
@@ -34,6 +34,7 @@ import org.springframework.util.CollectionUtils;
/**
* @author Ryan Baxter
* @author Isik Erhan
*/
public class KubernetesClientConfigMapPropertySource extends ConfigMapPropertySource {
@@ -42,18 +43,19 @@ public class KubernetesClientConfigMapPropertySource extends ConfigMapPropertySo
@Deprecated
public KubernetesClientConfigMapPropertySource(CoreV1Api coreV1Api, String name, String namespace,
Environment environment) {
super(getName(name, namespace), getData(coreV1Api, name, namespace, environment, "", true));
super(getName(name, namespace), getData(coreV1Api, name, namespace, environment, "", true, false));
}
public KubernetesClientConfigMapPropertySource(CoreV1Api coreV1Api, String name, String namespace,
Environment environment, String prefix, boolean includeProfileSpecificSources) {
Environment environment, String prefix, boolean includeProfileSpecificSources, boolean failFast) {
super(getName(name, namespace),
getData(coreV1Api, name, namespace, environment, prefix, includeProfileSpecificSources));
getData(coreV1Api, name, namespace, environment, prefix, includeProfileSpecificSources, failFast));
}
private static Map<String, Object> getData(CoreV1Api coreV1Api, String name, String namespace,
Environment environment, String prefix, boolean includeProfileSpecificSources) {
Environment environment, String prefix, boolean includeProfileSpecificSources, boolean failFast) {
LOG.info("Loading ConfigMap with name '" + name + "' in namespace '" + namespace + "'");
try {
Set<String> names = new HashSet<>();
names.add(name);
@@ -77,6 +79,11 @@ public class KubernetesClientConfigMapPropertySource extends ConfigMapPropertySo
return result;
}
catch (ApiException e) {
if (failFast) {
throw new IllegalStateException(
"Unable to read ConfigMap with name '" + name + "' in namespace '" + namespace + "'", e);
}
LOG.warn("Unable to get ConfigMap " + name + " in namespace " + namespace, e);
}
return Collections.emptyMap();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2020 the original author or authors.
* 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.
@@ -29,6 +29,7 @@ import org.springframework.util.StringUtils;
/**
* @author Ryan Baxter
* @author Isik Erhan
*/
public class KubernetesClientConfigMapPropertySourceLocator extends ConfigMapPropertySourceLocator {
@@ -88,7 +89,8 @@ public class KubernetesClientConfigMapPropertySourceLocator extends ConfigMapPro
}
return new KubernetesClientConfigMapPropertySource(coreV1Api, name, namespace, environment,
normalizedSource.getPrefix(), normalizedSource.isIncludeProfileSpecificSources());
normalizedSource.getPrefix(), normalizedSource.isIncludeProfileSpecificSources(),
this.properties.isFailFast());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2020 the original author or authors.
* 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.
@@ -32,6 +32,7 @@ import org.springframework.util.StringUtils;
/**
* @author Ryan Baxter
* @author Isik Erhan
*/
public class KubernetesClientSecretsPropertySource extends SecretsPropertySource {
@@ -40,15 +41,17 @@ public class KubernetesClientSecretsPropertySource extends SecretsPropertySource
private CoreV1Api coreV1Api;
public KubernetesClientSecretsPropertySource(CoreV1Api coreV1Api, String name, String namespace,
Environment environment, Map<String, String> labels) {
super(getSourceName(name, namespace), getSourceData(coreV1Api, environment, name, namespace, labels));
Environment environment, Map<String, String> labels, boolean failFast) {
super(getSourceName(name, namespace), getSourceData(coreV1Api, environment, name, namespace, labels, failFast));
}
private static Map<String, Object> getSourceData(CoreV1Api api, Environment env, String name, String namespace,
Map<String, String> labels) {
Map<String, String> labels, boolean failFast) {
Map<String, Object> result = new HashMap<>();
LOG.info("Loading Secret with name '" + name + "' or with labels [" + labels + "] in namespace '" + namespace
+ "'");
try {
// Read for secrets api (named)
if (StringUtils.hasText(name)) {
@@ -81,6 +84,11 @@ public class KubernetesClientSecretsPropertySource extends SecretsPropertySource
}
}
catch (Exception e) {
if (failFast) {
throw new IllegalStateException("Unable to read Secret with name '" + name + "' or labels [" + labels
+ "] in namespace '" + namespace + "'", e);
}
LOG.warn("Can't read secret with name: [" + name + "] or labels [" + labels + "] in namespace:[" + namespace
+ "] (cause: " + e.getMessage() + "). Ignoring", e);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2020 the original author or authors.
* 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.
@@ -31,6 +31,7 @@ import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.ge
/**
* @author Ryan Baxter
* @author Isik Erhan
*/
public class KubernetesClientSecretsPropertySourceLocator extends SecretsPropertySourceLocator {
@@ -90,7 +91,7 @@ public class KubernetesClientSecretsPropertySourceLocator extends SecretsPropert
}
return new KubernetesClientSecretsPropertySource(coreV1Api, secretName, namespace, environment,
normalizedSource.getLabels());
normalizedSource.getLabels(), this.properties.isFailFast());
}
}

View File

@@ -0,0 +1,278 @@
/*
* 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 java.util.HashMap;
import java.util.Map;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import io.kubernetes.client.openapi.JSON;
import io.kubernetes.client.openapi.models.V1ConfigMap;
import io.kubernetes.client.openapi.models.V1ConfigMapList;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.openapi.models.V1SecretList;
import io.kubernetes.client.util.ClientBuilder;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.cloud.kubernetes.client.KubernetesClientUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.core.env.PropertySource;
import org.springframework.mock.env.MockEnvironment;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
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 com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* @author Isik Erhan
*/
public class KubernetesClientConfigMapPropertySourceLocatorRetryTests {
private static final String API = "/api/v1/namespaces/default/configmaps";
private static final String SECRETS_API = "/api/v1/namespaces/default/secrets";
private static WireMockServer wireMockServer;
private static MockedStatic<KubernetesClientUtils> clientUtilsMock;
@BeforeAll
public static void setup() {
wireMockServer = new WireMockServer(options().dynamicPort());
wireMockServer.start();
WireMock.configureFor(wireMockServer.port());
clientUtilsMock = mockStatic(KubernetesClientUtils.class);
clientUtilsMock.when(KubernetesClientUtils::kubernetesApiClient)
.thenReturn(new ClientBuilder().setBasePath(wireMockServer.baseUrl()).build());
stubConfigMapAndSecretsDefaults();
}
private static void stubConfigMapAndSecretsDefaults() {
// return empty config map / secret list to not fail context creation
stubFor(get(API).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(new V1ConfigMapList()))));
stubFor(get(SECRETS_API)
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(new V1SecretList()))));
}
@AfterAll
public static void teardown() {
wireMockServer.stop();
clientUtilsMock.close();
}
@AfterEach
public void afterEach() {
WireMock.reset();
stubConfigMapAndSecretsDefaults();
}
@Nested
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
properties = { "spring.cloud.kubernetes.client.namespace=default",
"spring.cloud.kubernetes.config.fail-fast=true",
"spring.cloud.kubernetes.config.retry.max-attempts=5" },
classes = App.class)
class ConfigRetryEnabled {
@SpyBean
private KubernetesClientConfigMapPropertySourceLocator propertySourceLocator;
@Test
public void locateShouldNotRetryWhenThereIsNoFailure() {
Map<String, String> data = new HashMap<>();
data.put("some.prop", "theValue");
data.put("some.number", "0");
V1ConfigMapList configMapList = new V1ConfigMapList()
.addItemsItem(new V1ConfigMap().metadata(new V1ObjectMeta().name("application")).data(data));
stubFor(get(API).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(configMapList))));
PropertySource<?> propertySource = Assertions
.assertDoesNotThrow(() -> propertySourceLocator.locate(new MockEnvironment()));
// verify locate is called only once
verify(propertySourceLocator, times(1)).locate(any());
// validate the contents of the property source
assertThat(propertySource.getProperty("some.prop")).isEqualTo("theValue");
assertThat(propertySource.getProperty("some.number")).isEqualTo("0");
}
@Test
public void locateShouldRetryAndRecover() {
Map<String, String> data = new HashMap<>();
data.put("some.prop", "theValue");
data.put("some.number", "0");
V1ConfigMapList configMapList = new V1ConfigMapList()
.addItemsItem(new V1ConfigMap().metadata(new V1ObjectMeta().name("application")).data(data));
// fail 3 times
stubFor(get(API).inScenario("Retry and Recover").whenScenarioStateIs(STARTED)
.willReturn(aResponse().withStatus(500)).willSetStateTo("Failed once"));
stubFor(get(API).inScenario("Retry and Recover").whenScenarioStateIs("Failed once")
.willReturn(aResponse().withStatus(500)).willSetStateTo("Failed twice"));
stubFor(get(API).inScenario("Retry and Recover").whenScenarioStateIs("Failed twice")
.willReturn(aResponse().withStatus(500)).willSetStateTo("Failed thrice"));
// then succeed
stubFor(get(API).inScenario("Retry and Recover").whenScenarioStateIs("Failed thrice")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(configMapList))));
PropertySource<?> propertySource = Assertions
.assertDoesNotThrow(() -> propertySourceLocator.locate(new MockEnvironment()));
// verify retried 4 times
verify(propertySourceLocator, times(4)).locate(any());
// validate the contents of the property source
assertThat(propertySource.getProperty("some.prop")).isEqualTo("theValue");
assertThat(propertySource.getProperty("some.number")).isEqualTo("0");
}
@Test
public void locateShouldRetryAndFail() {
// fail all the 5 requests
stubFor(get(API).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")));
assertThatThrownBy(() -> propertySourceLocator.locate(new MockEnvironment()))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read ConfigMap with name 'application' in namespace 'default'");
// verify retried 5 times until failure
verify(propertySourceLocator, times(5)).locate(any());
}
}
@Nested
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
properties = { "spring.cloud.kubernetes.client.namespace=default" }, classes = App.class)
class ConfigFailFastDisabled {
@SpyBean
private KubernetesClientConfigMapPropertySourceLocator propertySourceLocator;
@Test
public void locateShouldNotRetry() {
stubFor(get(API).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")));
Assertions.assertDoesNotThrow(() -> propertySourceLocator.locate(new MockEnvironment()));
// verify locate is called only once
verify(propertySourceLocator, times(1)).locate(any());
}
}
@Nested
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, properties = {
"spring.cloud.kubernetes.client.namespace=default", "spring.cloud.kubernetes.config.fail-fast=true",
"spring.cloud.kubernetes.config.retry.enabled=false", "spring.cloud.kubernetes.secrets.fail-fast=true" },
classes = App.class)
class ConfigRetryDisabledButSecretsRetryEnabled {
@SpyBean
private KubernetesClientConfigMapPropertySourceLocator propertySourceLocator;
@Autowired
private ApplicationContext context;
@Test
public void locateShouldFailWithoutRetrying() {
/*
* Enabling secrets retry causes Spring Retry to be enabled and a
* RetryOperationsInterceptor bean with NeverRetryPolicy for config maps to be
* defined. ConfigMapPropertySourceLocator should not retry even Spring Retry
* is enabled.
*/
stubFor(get(API).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")));
assertThat(context.containsBean("kubernetesConfigRetryInterceptor")).isTrue();
assertThatThrownBy(() -> propertySourceLocator.locate(new MockEnvironment()))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read ConfigMap with name 'application' in namespace 'default'");
// verify that propertySourceLocator.locate is called only once
verify(propertySourceLocator, times(1)).locate(any());
}
}
@Nested
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
properties = { "spring.cloud.kubernetes.client.namespace=default",
"spring.cloud.kubernetes.config.fail-fast=true",
"spring.cloud.kubernetes.config.retry.enabled=false" },
classes = App.class)
class ConfigFailFastEnabledButRetryDisabled {
@SpyBean
private KubernetesClientConfigMapPropertySourceLocator propertySourceLocator;
@Autowired
private ApplicationContext context;
@Test
public void locateShouldFailWithoutRetrying() {
stubFor(get(API).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")));
assertThat(context.containsBean("kubernetesConfigRetryInterceptor")).isFalse();
assertThatThrownBy(() -> propertySourceLocator.locate(new MockEnvironment()))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read ConfigMap with name 'application' in namespace 'default'");
// verify that propertySourceLocator.locate is called only once
verify(propertySourceLocator, times(1)).locate(any());
}
}
@SpringBootApplication
static class App {
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2020 the original author or authors.
* 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.
@@ -46,10 +46,12 @@ 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.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* @author Ryan Baxter
* @author Isik Erhan
*/
class KubernetesClientConfigMapPropertySourceLocatorTests {
@@ -166,4 +168,39 @@ class KubernetesClientConfigMapPropertySourceLocatorTests {
.isInstanceOf(NamespaceResolutionFailedException.class);
}
@Test
public void locateShouldThrowExceptionOnFailureWhenFailFastIsEnabled() {
CoreV1Api api = new CoreV1Api();
stubFor(get("/api/v1/namespaces/default/configmaps")
.willReturn(aResponse().withStatus(500).withBody("Internal Server Error")));
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties();
configMapConfigProperties.setName("bootstrap-640");
configMapConfigProperties.setNamespace("default");
configMapConfigProperties.setFailFast(true);
KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api,
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
assertThatThrownBy(() -> locator.locate(new MockEnvironment())).isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read ConfigMap with name 'bootstrap-640' in namespace 'default'");
}
@Test
public void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() {
CoreV1Api api = new CoreV1Api();
stubFor(get("/api/v1/namespaces/default/configmaps")
.willReturn(aResponse().withStatus(500).withBody("Internal Server Error")));
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties();
configMapConfigProperties.setName("bootstrap-640");
configMapConfigProperties.setNamespace("default");
configMapConfigProperties.setFailFast(false);
KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api,
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
assertThatNoException().isThrownBy(() -> locator.locate(new MockEnvironment()));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2020 the original author or authors.
* 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.
@@ -41,9 +41,12 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* @author Ryan Baxter
* @author Isik Erhan
*/
class KubernetesClientConfigMapPropertySourceTests {
@@ -95,7 +98,7 @@ class KubernetesClientConfigMapPropertySourceTests {
stubFor(get("/api/v1/namespaces/default/configmaps")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(PROPERTIES_CONFIGMAP_LIST))));
KubernetesClientConfigMapPropertySource propertySource = new KubernetesClientConfigMapPropertySource(api,
"bootstrap-640", "default", new MockEnvironment(), "", true);
"bootstrap-640", "default", new MockEnvironment(), "", true, false);
verify(getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps")));
assertThat(propertySource.containsProperty("spring.cloud.kubernetes.configuration.watcher.refreshDelay"))
.isTrue();
@@ -112,7 +115,7 @@ class KubernetesClientConfigMapPropertySourceTests {
stubFor(get("/api/v1/namespaces/default/configmaps")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(YAML_CONFIGMAP_LIST))));
KubernetesClientConfigMapPropertySource propertySource = new KubernetesClientConfigMapPropertySource(api,
"bootstrap-641", "default", new MockEnvironment(), "", true);
"bootstrap-641", "default", new MockEnvironment(), "", true, false);
verify(getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps")));
assertThat(propertySource.containsProperty("dummy.property.string2")).isTrue();
assertThat(propertySource.getProperty("dummy.property.string2")).isEqualTo("a");
@@ -129,7 +132,7 @@ class KubernetesClientConfigMapPropertySourceTests {
stubFor(get("/api/v1/namespaces/default/configmaps")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(PROPERTIES_CONFIGMAP_LIST))));
KubernetesClientConfigMapPropertySource propertySource = new KubernetesClientConfigMapPropertySource(api,
"bootstrap-640", "default", new MockEnvironment(), "prefix", true);
"bootstrap-640", "default", new MockEnvironment(), "prefix", true, false);
verify(getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps")));
assertThat(propertySource.containsProperty("prefix.spring.cloud.kubernetes.configuration.watcher.refreshDelay"))
.isTrue();
@@ -141,4 +144,27 @@ class KubernetesClientConfigMapPropertySourceTests {
.isEqualTo("TRACE");
}
@Test
public void constructorShouldThrowExceptionOnFailureWhenFailFastIsEnabled() {
CoreV1Api api = new CoreV1Api();
stubFor(get("/api/v1/namespaces/default/configmaps")
.willReturn(aResponse().withStatus(500).withBody("Internal Server Error")));
assertThatThrownBy(() -> new KubernetesClientConfigMapPropertySource(api, "my-config", "default",
new MockEnvironment(), "", false, true)).isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read ConfigMap with name 'my-config' in namespace 'default'");
verify(getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps")));
}
@Test
public void constructorShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() {
CoreV1Api api = new CoreV1Api();
stubFor(get("/api/v1/namespaces/default/configmaps")
.willReturn(aResponse().withStatus(500).withBody("Internal Server Error")));
assertThatNoException().isThrownBy((() -> new KubernetesClientConfigMapPropertySource(api, "my-config",
"default", new MockEnvironment(), "", false, false)));
verify(getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps")));
}
}

View File

@@ -0,0 +1,280 @@
/*
* 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 java.util.HashMap;
import java.util.Map;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import io.kubernetes.client.openapi.JSON;
import io.kubernetes.client.openapi.models.V1ConfigMapList;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.openapi.models.V1Secret;
import io.kubernetes.client.openapi.models.V1SecretList;
import io.kubernetes.client.util.ClientBuilder;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.cloud.kubernetes.client.KubernetesClientUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.core.env.PropertySource;
import org.springframework.mock.env.MockEnvironment;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
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 com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* @author Isik Erhan
*/
public class KubernetesClientSecretsPropertySourceLocatorRetryTests {
private static final String API = "/api/v1/namespaces/default/secrets";
private static final String CONFIG_MAPS_API = "/api/v1/namespaces/default/configmaps";
private static WireMockServer wireMockServer;
private static MockedStatic<KubernetesClientUtils> clientUtilsMock;
@BeforeAll
public static void setup() {
wireMockServer = new WireMockServer(options().dynamicPort());
wireMockServer.start();
WireMock.configureFor(wireMockServer.port());
clientUtilsMock = mockStatic(KubernetesClientUtils.class);
clientUtilsMock.when(KubernetesClientUtils::kubernetesApiClient)
.thenReturn(new ClientBuilder().setBasePath(wireMockServer.baseUrl()).build());
stubConfigMapAndSecretsDefaults();
}
private static void stubConfigMapAndSecretsDefaults() {
// return empty config map / secret list to not fail context creation
stubFor(get(API).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(new V1SecretList()))));
stubFor(get(CONFIG_MAPS_API)
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(new V1ConfigMapList()))));
}
@AfterAll
public static void teardown() {
wireMockServer.stop();
clientUtilsMock.close();
}
@AfterEach
public void afterEach() {
WireMock.reset();
stubConfigMapAndSecretsDefaults();
}
@Nested
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, properties = {
"spring.cloud.kubernetes.client.namespace=default", "spring.cloud.kubernetes.secrets.fail-fast=true",
"spring.cloud.kubernetes.secrets.retry.max-attempts=5", "spring.cloud.kubernetes.secrets.name=my-secret",
"spring.cloud.kubernetes.secrets.enable-api=true" }, classes = App.class)
class SecretsRetryEnabled {
@SpyBean
private KubernetesClientSecretsPropertySourceLocator propertySourceLocator;
@Test
public void locateShouldNotRetryWhenThereIsNoFailure() {
Map<String, byte[]> data = new HashMap<>();
data.put("some.sensitive.prop", "theSensitiveValue".getBytes());
data.put("some.sensitive.number", "1".getBytes());
V1SecretList secretList = new V1SecretList()
.addItemsItem(new V1Secret().metadata(new V1ObjectMeta().name("my-secret")).data(data));
stubFor(get(API).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(secretList))));
PropertySource<?> propertySource = Assertions
.assertDoesNotThrow(() -> propertySourceLocator.locate(new MockEnvironment()));
// verify locate is called only once
verify(propertySourceLocator, times(1)).locate(any());
// validate the contents of the property source
assertThat(propertySource.getProperty("some.sensitive.prop")).isEqualTo("theSensitiveValue");
assertThat(propertySource.getProperty("some.sensitive.number")).isEqualTo("1");
}
@Test
public void locateShouldRetryAndRecover() {
Map<String, byte[]> data = new HashMap<>();
data.put("some.sensitive.prop", "theSensitiveValue".getBytes());
data.put("some.sensitive.number", "1".getBytes());
V1SecretList secretList = new V1SecretList()
.addItemsItem(new V1Secret().metadata(new V1ObjectMeta().name("my-secret")).data(data));
// fail 3 times
stubFor(get(API).inScenario("Retry and Recover").whenScenarioStateIs(STARTED)
.willReturn(aResponse().withStatus(500)).willSetStateTo("Failed once"));
stubFor(get(API).inScenario("Retry and Recover").whenScenarioStateIs("Failed once")
.willReturn(aResponse().withStatus(500)).willSetStateTo("Failed twice"));
stubFor(get(API).inScenario("Retry and Recover").whenScenarioStateIs("Failed twice")
.willReturn(aResponse().withStatus(500)).willSetStateTo("Failed thrice"));
// then succeed
stubFor(get(API).inScenario("Retry and Recover").whenScenarioStateIs("Failed thrice")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(secretList))));
PropertySource<?> propertySource = Assertions
.assertDoesNotThrow(() -> propertySourceLocator.locate(new MockEnvironment()));
// verify retried 4 times
verify(propertySourceLocator, times(4)).locate(any());
// validate the contents of the property source
assertThat(propertySource.getProperty("some.sensitive.prop")).isEqualTo("theSensitiveValue");
assertThat(propertySource.getProperty("some.sensitive.number")).isEqualTo("1");
}
@Test
public void locateShouldRetryAndFail() {
// fail all the 5 requests
stubFor(get(API).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")));
assertThatThrownBy(() -> propertySourceLocator.locate(new MockEnvironment()))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read Secret with name 'my-secret' or labels [{}] in namespace 'default'");
// verify retried 5 times until failure
verify(propertySourceLocator, times(5)).locate(any());
}
}
@Nested
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
properties = { "spring.cloud.kubernetes.client.namespace=default",
"spring.cloud.kubernetes.secrets.name=my-secret",
"spring.cloud.kubernetes.secrets.enable-api=true" },
classes = App.class)
class SecretsFailFastDisabled {
@SpyBean
private KubernetesClientSecretsPropertySourceLocator propertySourceLocator;
@Test
public void locateShouldNotRetry() {
stubFor(get(API).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")));
Assertions.assertDoesNotThrow(() -> propertySourceLocator.locate(new MockEnvironment()));
// verify locate is called only once
verify(propertySourceLocator, times(1)).locate(any());
}
}
@Nested
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, properties = {
"spring.cloud.kubernetes.client.namespace=default", "spring.cloud.kubernetes.secrets.fail-fast=true",
"spring.cloud.kubernetes.secrets.retry.enabled=false", "spring.cloud.kubernetes.config.fail-fast=true",
"spring.cloud.kubernetes.secrets.name=my-secret", "spring.cloud.kubernetes.secrets.enable-api=true" },
classes = App.class)
class SecretsRetryDisabledButConfigRetryEnabled {
@SpyBean
private KubernetesClientSecretsPropertySourceLocator propertySourceLocator;
@Autowired
private ApplicationContext context;
@Test
public void locateShouldFailWithoutRetrying() {
/*
* Enabling config retry causes Spring Retry to be enabled and a
* RetryOperationsInterceptor bean with NeverRetryPolicy for secrets to be
* defined. SecretsPropertySourceLocator should not retry even Spring Retry is
* enabled.
*/
stubFor(get(API).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")));
assertThat(context.containsBean("kubernetesSecretsRetryInterceptor")).isTrue();
assertThatThrownBy(() -> propertySourceLocator.locate(new MockEnvironment()))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read Secret with name 'my-secret' or labels [{}] in namespace 'default'");
// verify that propertySourceLocator.locate is called only once
verify(propertySourceLocator, times(1)).locate(any());
}
}
@Nested
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, properties = {
"spring.cloud.kubernetes.client.namespace=default", "spring.cloud.kubernetes.secrets.fail-fast=true",
"spring.cloud.kubernetes.secrets.retry.enabled=false", "spring.cloud.kubernetes.secrets.name=my-secret",
"spring.cloud.kubernetes.secrets.enable-api=true" }, classes = App.class)
class SecretsFailFastEnabledButRetryDisabled {
@SpyBean
private KubernetesClientSecretsPropertySourceLocator propertySourceLocator;
@Autowired
private ApplicationContext context;
@Test
public void locateShouldFailWithoutRetrying() {
stubFor(get(API).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")));
assertThat(context.containsBean("kubernetesSecretsRetryInterceptor")).isFalse();
assertThatThrownBy(() -> propertySourceLocator.locate(new MockEnvironment()))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read Secret with name 'my-secret' or labels [{}] in namespace 'default'");
// verify that propertySourceLocator.locate is called only once
verify(propertySourceLocator, times(1)).locate(any());
}
}
@SpringBootApplication
static class App {
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2020 the original author or authors.
* 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.
@@ -42,10 +42,12 @@ 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.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* @author Ryan Baxter
* @author Isik Erhan
*/
class KubernetesClientSecretsPropertySourceLocatorTests {
@@ -178,4 +180,39 @@ class KubernetesClientSecretsPropertySourceLocatorTests {
.locate(new MockEnvironment())).isInstanceOf(NamespaceResolutionFailedException.class);
}
@Test
public void locateShouldThrowExceptionOnFailureWhenFailFastIsEnabled() {
CoreV1Api api = new CoreV1Api();
stubFor(get(LIST_API).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")));
SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties();
secretsConfigProperties.setName("db-secret");
secretsConfigProperties.setNamespace("default");
secretsConfigProperties.setEnableApi(true);
secretsConfigProperties.setFailFast(true);
KubernetesClientSecretsPropertySourceLocator locator = new KubernetesClientSecretsPropertySourceLocator(api,
new KubernetesNamespaceProvider(new MockEnvironment()), secretsConfigProperties);
assertThatThrownBy(() -> locator.locate(new MockEnvironment())).isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read Secret with name 'db-secret' or labels [{}] in namespace 'default'");
}
@Test
public void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() {
CoreV1Api api = new CoreV1Api();
stubFor(get(LIST_API).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")));
SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties();
secretsConfigProperties.setName("db-secret");
secretsConfigProperties.setNamespace("default");
secretsConfigProperties.setEnableApi(true);
secretsConfigProperties.setFailFast(false);
KubernetesClientSecretsPropertySourceLocator locator = new KubernetesClientSecretsPropertySourceLocator(api,
new KubernetesNamespaceProvider(new MockEnvironment()), secretsConfigProperties);
assertThatNoException().isThrownBy(() -> locator.locate(new MockEnvironment()));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2020 the original author or authors.
* 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.
@@ -39,12 +39,18 @@ import org.springframework.mock.env.MockEnvironment;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* @author Ryan Baxter
* @author Isik Erhan
*/
class KubernetesClientSecretsPropertySourceTests {
@@ -111,7 +117,7 @@ class KubernetesClientSecretsPropertySourceTests {
CoreV1Api api = new CoreV1Api();
stubFor(get(API).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(SECRET_LIST))));
KubernetesClientSecretsPropertySource propertySource = new KubernetesClientSecretsPropertySource(api,
"db-secret", "default", new MockEnvironment(), new HashMap<>());
"db-secret", "default", new MockEnvironment(), new HashMap<>(), false);
assertThat(propertySource.containsProperty("password")).isTrue();
assertThat(propertySource.getProperty("password")).isEqualTo("p455w0rd");
assertThat(propertySource.containsProperty("username")).isTrue();
@@ -123,7 +129,7 @@ class KubernetesClientSecretsPropertySourceTests {
CoreV1Api api = new CoreV1Api();
stubFor(get(LIST_API).willReturn(aResponse().withStatus(200).withBody(LIST_BODY)));
KubernetesClientSecretsPropertySource propertySource = new KubernetesClientSecretsPropertySource(api,
"db-secret", null, new MockEnvironment(), new HashMap<>());
"db-secret", null, new MockEnvironment(), new HashMap<>(), false);
assertThat(propertySource.containsProperty("password")).isTrue();
assertThat(propertySource.getProperty("password")).isEqualTo("p455w0rd");
assertThat(propertySource.containsProperty("username")).isTrue();
@@ -137,9 +143,30 @@ class KubernetesClientSecretsPropertySourceTests {
Map<String, String> labels = new HashMap<>();
labels.put("spring.cloud.kubernetes.secret", "true");
KubernetesClientSecretsPropertySource propertySource = new KubernetesClientSecretsPropertySource(api, null,
null, new MockEnvironment(), labels);
null, new MockEnvironment(), labels, false);
assertThat(propertySource.containsProperty("spring.rabbitmq.password")).isTrue();
assertThat(propertySource.getProperty("spring.rabbitmq.password")).isEqualTo("password");
}
@Test
public void constructorShouldThrowExceptionOnFailureWhenFailFastIsEnabled() {
CoreV1Api api = new CoreV1Api();
stubFor(get(API).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")));
assertThatThrownBy(() -> new KubernetesClientSecretsPropertySource(api, "db-secret", "default",
new MockEnvironment(), null, true)).isInstanceOf(IllegalStateException.class).hasMessage(
"Unable to read Secret with name 'db-secret' or labels [null] in namespace 'default'");
verify(getRequestedFor(urlEqualTo(API)));
}
@Test
public void constructorShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() {
CoreV1Api api = new CoreV1Api();
stubFor(get(API).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")));
assertThatNoException().isThrownBy((() -> new KubernetesClientSecretsPropertySource(api, "db-secret", "default",
new MockEnvironment(), null, false)));
verify(getRequestedFor(urlEqualTo(API)));
}
}

View File

@@ -57,6 +57,17 @@
<artifactId>spring-boot-starter-logging</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2019 the original author or authors.
* 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.
@@ -20,6 +20,7 @@ package org.springframework.cloud.kubernetes.commons.config;
* Abstraction over configuration properties.
*
* @author Ioannis Canellos
* @author Isik Erhan
*/
public abstract class AbstractConfigProperties {
@@ -35,6 +36,10 @@ public abstract class AbstractConfigProperties {
// use profile name to append config map name
protected boolean includeProfileSpecificSources = true;
protected boolean failFast = false;
protected RetryProperties retry = new RetryProperties();
public abstract String getConfigurationTarget();
public boolean isEnabled() {
@@ -77,4 +82,79 @@ public abstract class AbstractConfigProperties {
this.includeProfileSpecificSources = includeProfileSpecificSources;
}
public boolean isFailFast() {
return failFast;
}
public void setFailFast(boolean failFast) {
this.failFast = failFast;
}
public RetryProperties getRetry() {
return retry;
}
public void setRetry(RetryProperties retry) {
this.retry = retry;
}
/**
* Kubernetes config retry properties.
*/
public static class RetryProperties {
/**
* Initial retry interval in milliseconds.
*/
private long initialInterval = 1000;
/**
* Multiplier for next interval.
*/
private double multiplier = 1.1;
/**
* Maximum interval for backoff.
*/
private long maxInterval = 2000;
/**
* Maximum number of attempts.
*/
private int maxAttempts = 6;
public long getInitialInterval() {
return this.initialInterval;
}
public void setInitialInterval(long initialInterval) {
this.initialInterval = initialInterval;
}
public double getMultiplier() {
return this.multiplier;
}
public void setMultiplier(double multiplier) {
this.multiplier = multiplier;
}
public long getMaxInterval() {
return this.maxInterval;
}
public void setMaxInterval(long maxInterval) {
this.maxInterval = maxInterval;
}
public int getMaxAttempts() {
return this.maxAttempts;
}
public void setMaxAttempts(int maxAttempts) {
this.maxAttempts = maxAttempts;
}
}
}

View File

@@ -0,0 +1,41 @@
/*
* 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;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
/**
* Provides a more succinct conditional
* <code>spring.cloud.kubernetes.config.fail-fast</code>.
*
* @author Isik Erhan
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@ConditionalOnProperty(prefix = ConfigMapConfigProperties.PREFIX, name = "fail-fast", havingValue = "true")
public @interface ConditionalOnKubernetesConfigFailFastEnabled {
}

View File

@@ -0,0 +1,62 @@
/*
* 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;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.context.annotation.Conditional;
/**
* {@link Conditional @Conditional} that matches when either or both of
* {@link ConditionalOnKubernetesConfigRetryEnabled @ConditionalOnKubernetesConfigRetryEnabled}
* and
* {@link ConditionalOnKubernetesSecretsRetryEnabled @ConditionalOnKubernetesSecretsRetryEnabled}.
*
* @author Isik Erhan
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Conditional(ConditionalOnKubernetesConfigOrSecretsRetryEnabled.OnKubernetesConfigPropertiesRetryEnabled.class)
public @interface ConditionalOnKubernetesConfigOrSecretsRetryEnabled {
class OnKubernetesConfigPropertiesRetryEnabled extends AnyNestedCondition {
OnKubernetesConfigPropertiesRetryEnabled() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnKubernetesConfigRetryEnabled
static class OnConfigMapPropertiesRetryEnabled {
}
@ConditionalOnKubernetesSecretsRetryEnabled
static class OnSecretsPropertiesRetryEnabled {
}
}
}

View File

@@ -0,0 +1,56 @@
/*
* 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;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
import org.springframework.context.annotation.Conditional;
/**
* {@link Conditional @Conditional} that matches when at least one of Spring Cloud
* Kubernetes, Kubernetes ConfigMap property sources or Kubernetes ConfigMap property
* sources fail fast (thus retry) is disabled.
*
* @author Isik Erhan
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Conditional(ConditionalOnKubernetesConfigRetryDisabled.OnConfigMapPropertiesRetryDisabled.class)
public @interface ConditionalOnKubernetesConfigRetryDisabled {
class OnConfigMapPropertiesRetryDisabled extends NoneNestedConditions {
OnConfigMapPropertiesRetryDisabled() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnKubernetesConfigRetryEnabled
static class OnConfigMapPropertiesRetryEnabled {
}
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.kubernetes.commons.ConditionalOnKubernetesConfigEnabled;
import org.springframework.cloud.kubernetes.commons.ConditionalOnKubernetesEnabled;
/**
* {@link org.springframework.context.annotation.Conditional @Conditional} that only
* matches when Spring Cloud Kubernetes, Kubernetes config, Kubernetes config fail-fast
* and Kubernetes config retry are enabled.
*
* @author Isik Erhan
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@ConditionalOnKubernetesEnabled
@ConditionalOnKubernetesConfigEnabled
@ConditionalOnKubernetesConfigFailFastEnabled
@ConditionalOnProperty(prefix = ConfigMapConfigProperties.PREFIX + ".retry", name = "enabled", havingValue = "true",
matchIfMissing = true)
public @interface ConditionalOnKubernetesConfigRetryEnabled {
}

View File

@@ -0,0 +1,41 @@
/*
* 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;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
/**
* Provides a more succinct conditional
* <code>spring.cloud.kubernetes.secrets.fail-fast</code>.
*
* @author Isik Erhan
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@ConditionalOnProperty(prefix = SecretsConfigProperties.PREFIX, name = "fail-fast", havingValue = "true")
public @interface ConditionalOnKubernetesSecretsFailFastEnabled {
}

View File

@@ -0,0 +1,56 @@
/*
* 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;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
import org.springframework.context.annotation.Conditional;
/**
* {@link Conditional @Conditional} that matches when at least one of Spring Cloud
* Kubernetes, Kubernetes Secret property sources or Kubernetes Secret property sources
* fail fast (thus retry) is disabled.
*
* @author Isik Erhan
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Conditional(ConditionalOnKubernetesSecretsRetryDisabled.OnSecretsPropertiesRetryDisabled.class)
public @interface ConditionalOnKubernetesSecretsRetryDisabled {
class OnSecretsPropertiesRetryDisabled extends NoneNestedConditions {
OnSecretsPropertiesRetryDisabled() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnKubernetesSecretsRetryEnabled
static class OnSecretsPropertiesRetryEnabled {
}
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.kubernetes.commons.ConditionalOnKubernetesEnabled;
import org.springframework.cloud.kubernetes.commons.ConditionalOnKubernetesSecretsEnabled;
/**
* {@link org.springframework.context.annotation.Conditional @Conditional} that only
* matches when Spring Cloud Kubernetes, Kubernetes secrets, Kubernetes secrets fail-fast
* and Kubernetes secrets retry are enabled.
*
* @author Isik Erhan
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@ConditionalOnKubernetesEnabled
@ConditionalOnKubernetesSecretsEnabled
@ConditionalOnKubernetesSecretsFailFastEnabled
@ConditionalOnProperty(prefix = SecretsConfigProperties.PREFIX + ".retry", name = "enabled", havingValue = "true",
matchIfMissing = true)
public @interface ConditionalOnKubernetesSecretsRetryEnabled {
}

View File

@@ -31,10 +31,16 @@ import org.springframework.util.StringUtils;
* Config map configuration properties.
*
* @author Ioannis Canellos
* @author Isik Erhan
*/
@ConfigurationProperties("spring.cloud.kubernetes.config")
@ConfigurationProperties(ConfigMapConfigProperties.PREFIX)
public class ConfigMapConfigProperties extends AbstractConfigProperties {
/**
* Prefix for Kubernetes secrets configuration properties.
*/
public static final String PREFIX = "spring.cloud.kubernetes.config";
private static final Log LOG = LogFactory.getLog(ConfigMapConfigProperties.class);
private boolean enableApi = true;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2019 the original author or authors.
* 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.
@@ -19,6 +19,7 @@ package org.springframework.cloud.kubernetes.commons.config;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
@@ -36,6 +37,7 @@ import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.retry.annotation.Retryable;
import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.getApplicationName;
import static org.springframework.cloud.kubernetes.commons.config.PropertySourceUtils.KEY_VALUE_TO_PROPERTIES;
@@ -47,6 +49,7 @@ import static org.springframework.cloud.kubernetes.commons.config.PropertySource
*
* @author Ioannis Canellos
* @author Michael Moudatsos
* @author Isik Erhan
*/
public abstract class ConfigMapPropertySourceLocator implements PropertySourceLocator {
@@ -62,6 +65,7 @@ public abstract class ConfigMapPropertySourceLocator implements PropertySourceLo
String configurationTarget, ConfigurableEnvironment environment);
@Override
@Retryable(interceptor = "kubernetesConfigRetryInterceptor")
public PropertySource<?> locate(Environment environment) {
if (environment instanceof ConfigurableEnvironment) {
ConfigurableEnvironment env = (ConfigurableEnvironment) environment;
@@ -80,6 +84,12 @@ public abstract class ConfigMapPropertySourceLocator implements PropertySourceLo
return null;
}
@Override
@Retryable(interceptor = "kubernetesConfigRetryInterceptor")
public Collection<PropertySource<?>> locateCollection(Environment environment) {
return PropertySourceLocator.super.locateCollection(environment);
}
private MapPropertySource getMapPropertySourceForSingleConfigMap(ConfigurableEnvironment environment,
NormalizedSource normalizedSource) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2020 the original author or authors.
* 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.
@@ -16,16 +16,68 @@
package org.springframework.cloud.kubernetes.commons.config;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.kubernetes.commons.ConditionalOnKubernetesEnabled;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.retry.annotation.Retryable;
import org.springframework.retry.interceptor.RetryInterceptorBuilder;
import org.springframework.retry.interceptor.RetryOperationsInterceptor;
import org.springframework.retry.policy.NeverRetryPolicy;
/**
* @author Ryan Baxter
* @author Isik Erhan
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnKubernetesEnabled
@EnableConfigurationProperties({ ConfigMapConfigProperties.class, SecretsConfigProperties.class })
public class KubernetesBootstrapConfiguration {
@ConditionalOnKubernetesConfigOrSecretsRetryEnabled
@ConditionalOnClass({ Retryable.class, Aspect.class, AopAutoConfiguration.class })
@Configuration(proxyBeanMethods = false)
@EnableRetry(proxyTargetClass = true)
@Import(AopAutoConfiguration.class)
static class RetryConfiguration {
private static RetryOperationsInterceptor retryOperationsInterceptor(
AbstractConfigProperties.RetryProperties retryProperties) {
return RetryInterceptorBuilder.stateless().backOffOptions(retryProperties.getInitialInterval(),
retryProperties.getMultiplier(), retryProperties.getMaxInterval())
.maxAttempts(retryProperties.getMaxAttempts()).build();
}
@Bean
@ConditionalOnKubernetesConfigRetryEnabled
public RetryOperationsInterceptor kubernetesConfigRetryInterceptor(ConfigMapConfigProperties configProperties) {
return retryOperationsInterceptor(configProperties.getRetry());
}
@Bean("kubernetesConfigRetryInterceptor")
@ConditionalOnKubernetesConfigRetryDisabled
public RetryOperationsInterceptor kubernetesConfigRetryInterceptorNoRetry() {
return RetryInterceptorBuilder.stateless().retryPolicy(new NeverRetryPolicy()).build();
}
@Bean
@ConditionalOnKubernetesSecretsRetryEnabled
public RetryOperationsInterceptor kubernetesSecretsRetryInterceptor(SecretsConfigProperties configProperties) {
return retryOperationsInterceptor(configProperties.getRetry());
}
@Bean("kubernetesSecretsRetryInterceptor")
@ConditionalOnKubernetesSecretsRetryDisabled
public RetryOperationsInterceptor kubernetesSecretsRetryInterceptorNoRetry() {
return RetryInterceptorBuilder.stateless().retryPolicy(new NeverRetryPolicy()).build();
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2019 the original author or authors.
* 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.
@@ -30,10 +30,16 @@ import org.springframework.util.StringUtils;
*
* @author l burgazzoli
* @author Haytham Mohamed
* @author Isik Erhan
*/
@ConfigurationProperties("spring.cloud.kubernetes.secrets")
@ConfigurationProperties(SecretsConfigProperties.PREFIX)
public class SecretsConfigProperties extends AbstractConfigProperties {
/**
* Prefix for Kubernetes secrets configuration properties.
*/
public static final String PREFIX = "spring.cloud.kubernetes.secrets";
private boolean enableApi = false;
private Map<String, String> labels = Collections.emptyMap();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2019 the original author or authors.
* 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.
@@ -21,6 +21,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
@@ -42,6 +43,7 @@ import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.retry.annotation.Retryable;
/**
* Kubernetes {@link PropertySourceLocator} for secrets.
@@ -49,6 +51,7 @@ import org.springframework.core.env.PropertySource;
* @author l burgazzoli
* @author Haytham Mohamed
* @author wind57
* @author Isik Erhan
*/
public abstract class SecretsPropertySourceLocator implements PropertySourceLocator {
@@ -61,6 +64,7 @@ public abstract class SecretsPropertySourceLocator implements PropertySourceLoca
}
@Override
@Retryable(interceptor = "kubernetesSecretsRetryInterceptor")
public PropertySource<?> locate(Environment environment) {
if (environment instanceof ConfigurableEnvironment) {
ConfigurableEnvironment env = (ConfigurableEnvironment) environment;
@@ -81,6 +85,12 @@ public abstract class SecretsPropertySourceLocator implements PropertySourceLoca
return null;
}
@Override
@Retryable(interceptor = "kubernetesSecretsRetryInterceptor")
public Collection<PropertySource<?>> locateCollection(Environment environment) {
return PropertySourceLocator.super.locateCollection(environment);
}
private MapPropertySource getMapPropertySourceForSingleSecret(ConfigurableEnvironment environment,
SecretsConfigProperties.NormalizedSource normalizedSource) {

View File

@@ -0,0 +1,280 @@
/*
* 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;
import java.util.Map;
import org.aspectj.lang.annotation.Aspect;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.kubernetes.commons.config.AbstractConfigProperties.RetryProperties;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.retry.annotation.Retryable;
import org.springframework.retry.interceptor.RetryOperationsInterceptor;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Isik Erhan
*/
public class KubernetesBootstrapConfigurationTests {
@SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class)
@Nested
public class FailFastDisabled {
@Autowired
ConfigurableApplicationContext context;
@Test
public void shouldNotDefineRetryBeans() {
assertThat(context.getBeansOfType(RetryOperationsInterceptor.class)).isEmpty();
}
}
@SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class,
properties = { "spring.cloud.kubernetes.config.fail-fast=true" })
@Nested
public class ConfigFailFastEnabled {
@Autowired
ConfigurableApplicationContext context;
@Autowired
ConfigMapConfigProperties configMapConfigProperties;
@Test
public void shouldDefineRequiredBeans() {
Map<String, RetryOperationsInterceptor> retryInterceptors = context
.getBeansOfType(RetryOperationsInterceptor.class);
assertThat(retryInterceptors.containsKey("kubernetesConfigRetryInterceptor")).isTrue();
assertThat(retryInterceptors.containsKey("kubernetesSecretsRetryInterceptor")).isTrue();
}
@Test
public void retryConfigurationShouldBeDefault() {
RetryProperties retryProperties = configMapConfigProperties.getRetry();
RetryProperties defaultRetryProperties = new RetryProperties();
assertThat(retryProperties.getMaxAttempts()).isEqualTo(defaultRetryProperties.getMaxAttempts());
assertThat(retryProperties.getInitialInterval()).isEqualTo(defaultRetryProperties.getInitialInterval());
assertThat(retryProperties.getMaxInterval()).isEqualTo(defaultRetryProperties.getMaxInterval());
assertThat(retryProperties.getMultiplier()).isEqualTo(defaultRetryProperties.getMultiplier());
}
}
@SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class,
properties = { "spring.cloud.kubernetes.secrets.fail-fast=true" })
@Nested
public class SecretsFailFastEnabled {
@Autowired
ConfigurableApplicationContext context;
@Autowired
SecretsConfigProperties secretsConfigProperties;
@Test
public void shouldDefineRequiredBeans() {
Map<String, RetryOperationsInterceptor> retryInterceptors = context
.getBeansOfType(RetryOperationsInterceptor.class);
assertThat(retryInterceptors.containsKey("kubernetesConfigRetryInterceptor")).isTrue();
assertThat(retryInterceptors.containsKey("kubernetesSecretsRetryInterceptor")).isTrue();
}
@Test
public void retryConfigurationShouldBeDefault() {
RetryProperties retryProperties = secretsConfigProperties.getRetry();
RetryProperties defaultRetryProperties = new RetryProperties();
assertThat(retryProperties.getMaxAttempts()).isEqualTo(defaultRetryProperties.getMaxAttempts());
assertThat(retryProperties.getInitialInterval()).isEqualTo(defaultRetryProperties.getInitialInterval());
assertThat(retryProperties.getMaxInterval()).isEqualTo(defaultRetryProperties.getMaxInterval());
assertThat(retryProperties.getMultiplier()).isEqualTo(defaultRetryProperties.getMultiplier());
}
}
@SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class, properties = {
"spring.cloud.kubernetes.config.fail-fast=true", "spring.cloud.kubernetes.secrets.fail-fast=true" })
@Nested
public class ConfigAndSecretsFailFastEnabledWithDefaultRetryConfiguration {
@Autowired
ConfigurableApplicationContext context;
@Autowired
ConfigMapConfigProperties configMapConfigProperties;
@Autowired
SecretsConfigProperties secretsConfigProperties;
@Test
public void shouldDefineRequiredBeans() {
Map<String, RetryOperationsInterceptor> retryInterceptors = context
.getBeansOfType(RetryOperationsInterceptor.class);
assertThat(retryInterceptors.containsKey("kubernetesConfigRetryInterceptor")).isTrue();
assertThat(retryInterceptors.containsKey("kubernetesSecretsRetryInterceptor")).isTrue();
}
@Test
public void retryConfigurationShouldBeDefault() {
RetryProperties defaultRetryProperties = new RetryProperties();
RetryProperties configMapRetryProperties = configMapConfigProperties.getRetry();
assertThat(configMapRetryProperties.getMaxAttempts()).isEqualTo(defaultRetryProperties.getMaxAttempts());
assertThat(configMapRetryProperties.getInitialInterval())
.isEqualTo(defaultRetryProperties.getInitialInterval());
assertThat(configMapRetryProperties.getMaxInterval()).isEqualTo(defaultRetryProperties.getMaxInterval());
assertThat(configMapRetryProperties.getMultiplier()).isEqualTo(defaultRetryProperties.getMultiplier());
RetryProperties secretsRetryProperties = secretsConfigProperties.getRetry();
assertThat(secretsRetryProperties.getMaxAttempts()).isEqualTo(defaultRetryProperties.getMaxAttempts());
assertThat(secretsRetryProperties.getInitialInterval())
.isEqualTo(defaultRetryProperties.getInitialInterval());
assertThat(secretsRetryProperties.getMaxInterval()).isEqualTo(defaultRetryProperties.getMaxInterval());
assertThat(secretsRetryProperties.getMultiplier()).isEqualTo(defaultRetryProperties.getMultiplier());
}
}
@SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class,
properties = { "spring.cloud.kubernetes.config.fail-fast=true",
"spring.cloud.kubernetes.config.retry.max-attempts=3",
"spring.cloud.kubernetes.config.retry.initial-interval=1500",
"spring.cloud.kubernetes.config.retry.max-interval=3000",
"spring.cloud.kubernetes.config.retry.multiplier=1.5" })
@Nested
public class ConfigFailFastEnabledWithCustomRetryConfiguration {
@Autowired
ConfigMapConfigProperties configMapConfigProperties;
@Test
public void retryConfigurationShouldBeCustomized() {
RetryProperties retryProperties = configMapConfigProperties.getRetry();
assertThat(retryProperties.getMaxAttempts()).isEqualTo(3);
assertThat(retryProperties.getInitialInterval()).isEqualTo(1500L);
assertThat(retryProperties.getMaxInterval()).isEqualTo(3000L);
assertThat(retryProperties.getMultiplier()).isEqualTo(1.5D);
}
}
@SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class,
properties = { "spring.cloud.kubernetes.secrets.fail-fast=true",
"spring.cloud.kubernetes.secrets.retry.max-attempts=3",
"spring.cloud.kubernetes.secrets.retry.initial-interval=1500",
"spring.cloud.kubernetes.secrets.retry.max-interval=3000",
"spring.cloud.kubernetes.secrets.retry.multiplier=1.5" })
@Nested
public class SecretsFailFastEnabledWithCustomRetryConfiguration {
@Autowired
SecretsConfigProperties secretsConfigProperties;
@Test
public void retryConfigurationShouldBeCustomized() {
RetryProperties retryProperties = secretsConfigProperties.getRetry();
assertThat(retryProperties.getMaxAttempts()).isEqualTo(3);
assertThat(retryProperties.getInitialInterval()).isEqualTo(1500L);
assertThat(retryProperties.getMaxInterval()).isEqualTo(3000L);
assertThat(retryProperties.getMultiplier()).isEqualTo(1.5D);
}
}
@SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class, properties = {
"spring.cloud.kubernetes.config.fail-fast=true", "spring.cloud.kubernetes.config.retry.enabled=false" })
@Nested
public class ConfigFailFastEnabledButRetryDisabled {
@Autowired
ConfigurableApplicationContext context;
@Test
public void shouldNotDefineRetryBeans() {
assertThat(context.getBeansOfType(RetryOperationsInterceptor.class)).isEmpty();
}
}
@SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class, properties = {
"spring.cloud.kubernetes.secrets.fail-fast=true", "spring.cloud.kubernetes.secrets.retry.enabled=false" })
@Nested
public class SecretsFailFastEnabledButRetryDisabled {
@Autowired
ConfigurableApplicationContext context;
@Test
public void shouldNotDefineRetryBeans() {
assertThat(context.getBeansOfType(RetryOperationsInterceptor.class)).isEmpty();
}
}
@Nested
public class FailFastEnabledWithoutSpringRetryOnClasspath {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(KubernetesBootstrapConfiguration.class))
.withClassLoader(new FilteredClassLoader(Retryable.class, Aspect.class, AopAutoConfiguration.class));
@Test
public void shouldNotDefineRetryBeansWhenConfigMapFailFastEnabled() {
contextRunner.withPropertyValues("spring.cloud.kubernetes.config.fail-fast=true")
.run(context -> assertThat(context.getBeansOfType(RetryOperationsInterceptor.class)).isEmpty());
}
@Test
public void shouldNotDefineRetryBeansWhenSecretsFailFastEnabled() {
contextRunner.withPropertyValues("spring.cloud.kubernetes.secrets.fail-fast=true")
.run(context -> assertThat(context.getBeansOfType(RetryOperationsInterceptor.class)).isEmpty());
}
@Test
public void shouldNotDefineRetryBeansWhenConfigMapAndSecretsFailFastEnabled() {
contextRunner
.withPropertyValues("spring.cloud.kubernetes.config.fail-fast=true",
"spring.cloud.kubernetes.secrets.fail-fast=true")
.run(context -> assertThat(context.getBeansOfType(RetryOperationsInterceptor.class)).isEmpty());
}
}
@SpringBootApplication
static class App {
}
}

View File

@@ -69,8 +69,8 @@ public class KubernetesConfigServerAutoConfiguration {
return (coreApi, applicationName, namespace, springEnv) -> {
List<String> namespaces = namespaceSplitter(properties.getSecretsNamespaces(), namespace);
List<MapPropertySource> propertySources = new ArrayList<>();
namespaces.forEach(space -> propertySources.add(
new KubernetesClientConfigMapPropertySource(coreApi, applicationName, space, springEnv, "", true)));
namespaces.forEach(space -> propertySources.add(new KubernetesClientConfigMapPropertySource(coreApi,
applicationName, space, springEnv, "", true, false)));
return propertySources;
};
}
@@ -83,7 +83,7 @@ public class KubernetesConfigServerAutoConfiguration {
List<String> namespaces = namespaceSplitter(properties.getSecretsNamespaces(), namespace);
List<MapPropertySource> propertySources = new ArrayList<>();
namespaces.forEach(space -> propertySources.add(new KubernetesClientSecretsPropertySource(coreApi,
applicationName, space, springEnv, new HashMap<>())));
applicationName, space, springEnv, new HashMap<>(), false)));
return propertySources;
};
}

View File

@@ -97,15 +97,15 @@ class KubernetesEnvironmentRepositoryTests {
kubernetesPropertySourceSuppliers.add((coreApi, applicationName, namespace, springEnv) -> {
List<MapPropertySource> propertySources = new ArrayList<>();
propertySources.add(new KubernetesClientConfigMapPropertySource(coreApi, applicationName, "default",
springEnv, "", true));
propertySources.add(
new KubernetesClientConfigMapPropertySource(coreApi, applicationName, "dev", springEnv, "", true));
springEnv, "", true, false));
propertySources.add(new KubernetesClientConfigMapPropertySource(coreApi, applicationName, "dev", springEnv,
"", true, false));
return propertySources;
});
kubernetesPropertySourceSuppliers.add((coreApi, applicationName, namespace, springEnv) -> {
List<MapPropertySource> propertySources = new ArrayList<>();
propertySources.add(new KubernetesClientSecretsPropertySource(coreApi, applicationName, "default",
springEnv, new HashMap<>()));
springEnv, new HashMap<>(), false));
return propertySources;
});
}

View File

@@ -40,6 +40,7 @@
<istio-client.version>1.7.7.1</istio-client.version>
<mockwebserver.version>0.1.2</mockwebserver.version>
<wiremock.version>2.26.3</wiremock.version>
<spring-retry.version>1.3.1</spring-retry.version>
</properties>
<dependencyManagement>
<dependencies>
@@ -73,6 +74,12 @@
<version>${istio-client.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>${spring-retry.version}</version>
</dependency>
<!-- Own dependencies -->
<dependency>
<groupId>org.springframework.cloud</groupId>

View File

@@ -179,6 +179,17 @@
<version>${groovy.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2019 the original author or authors.
* 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.
@@ -38,13 +38,14 @@ import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigU
* @author Ioannis Canellos
* @author Ali Shahbour
* @author Michael Moudatsos
* @author Isik Erhan
*/
public class Fabric8ConfigMapPropertySource extends ConfigMapPropertySource {
private static final Log LOG = LogFactory.getLog(Fabric8ConfigMapPropertySource.class);
public Fabric8ConfigMapPropertySource(KubernetesClient client, String name) {
this(client, name, null, null, "", true);
this(client, name, null, null, "", true, false);
}
/**
@@ -55,17 +56,20 @@ public class Fabric8ConfigMapPropertySource extends ConfigMapPropertySource {
public Fabric8ConfigMapPropertySource(KubernetesClient client, String name, String namespace,
Environment environment) {
super(getName(name, getApplicationNamespace(client, namespace)),
getData(client, name, getApplicationNamespace(client, namespace), environment, "", true));
getData(client, name, getApplicationNamespace(client, namespace), environment, "", true, false));
}
public Fabric8ConfigMapPropertySource(KubernetesClient client, String name, String namespace,
Environment environment, String prefix, boolean includeProfileSpecificSources) {
super(getName(name, getApplicationNamespace(client, namespace)), getData(client, name,
getApplicationNamespace(client, namespace), environment, prefix, includeProfileSpecificSources));
Environment environment, String prefix, boolean includeProfileSpecificSources, boolean failFast) {
super(getName(name, getApplicationNamespace(client, namespace)),
getData(client, name, getApplicationNamespace(client, namespace), environment, prefix,
includeProfileSpecificSources, failFast));
}
private static Map<String, Object> getData(KubernetesClient client, String name, String namespace,
Environment environment, String prefix, boolean includeProfileSpecificSources) {
Environment environment, String prefix, boolean includeProfileSpecificSources, boolean failFast) {
LOG.info("Loading ConfigMap with name '" + name + "' in namespace '" + namespace + "'");
try {
Map<String, String> data = getConfigMapData(client, namespace, name);
Map<String, Object> result = new HashMap<>(processAllEntries(data, environment));
@@ -88,6 +92,11 @@ public class Fabric8ConfigMapPropertySource extends ConfigMapPropertySource {
}
catch (Exception e) {
if (failFast) {
throw new IllegalStateException(
"Unable to read ConfigMap with name '" + name + "' in namespace '" + namespace + "'", e);
}
LOG.warn("Can't read configMap with name: [" + name + "] in namespace: [" + namespace + "]. Ignoring.", e);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2019 the original author or authors.
* 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.
@@ -34,6 +34,7 @@ import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigU
*
* @author Ioannis Canellos
* @author Michael Moudatsos
* @author Isik Erhan
*/
@Order(0)
public class Fabric8ConfigMapPropertySourceLocator extends ConfigMapPropertySourceLocator {
@@ -69,7 +70,8 @@ public class Fabric8ConfigMapPropertySourceLocator extends ConfigMapPropertySour
String namespace = getApplicationNamespace(this.client, normalizedSource.getNamespace(), configurationTarget,
provider);
return new Fabric8ConfigMapPropertySource(this.client, applicationName, namespace, environment,
normalizedSource.getPrefix(), normalizedSource.isIncludeProfileSpecificSources());
normalizedSource.getPrefix(), normalizedSource.isIncludeProfileSpecificSources(),
this.properties.isFailFast());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2019 the original author or authors.
* 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.
@@ -32,21 +32,24 @@ import org.springframework.util.StringUtils;
*
* @author l burgazzoli
* @author Haytham Mohamed
* @author Isik Erhan
*/
public class Fabric8SecretsPropertySource extends SecretsPropertySource {
private static final Log LOG = LogFactory.getLog(Fabric8SecretsPropertySource.class);
public Fabric8SecretsPropertySource(KubernetesClient client, String name, String namespace,
Map<String, String> labels) {
super(getSourceName(name, namespace), getSourceData(client, name, namespace, labels));
Map<String, String> labels, boolean failFast) {
super(getSourceName(name, namespace), getSourceData(client, name, namespace, labels, failFast));
}
private static Map<String, Object> getSourceData(KubernetesClient client, String name, String namespace,
Map<String, String> labels) {
Map<String, String> labels, boolean failFast) {
Map<String, Object> result = new HashMap<>();
String namespaceToUse = StringUtils.hasLength(namespace) ? namespace : client.getNamespace();
LOG.info("Loading Secret with name '" + name + "' or with labels [" + labels + "] in namespace '"
+ namespaceToUse + "'");
try {
Secret secret = client.secrets().inNamespace(namespaceToUse).withName(name).get();
@@ -64,6 +67,11 @@ public class Fabric8SecretsPropertySource extends SecretsPropertySource {
}
catch (Exception e) {
if (failFast) {
throw new IllegalStateException("Unable to read Secret with name '" + name + "' or labels [" + labels
+ "] in namespace '" + namespaceToUse + "'", e);
}
LOG.warn("Can't read secret with name: [" + name + "] or labels [" + labels + "] in namespace: ["
+ namespaceToUse + "] (cause: " + e.getMessage() + "). Ignoring");
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2019 the original author or authors.
* 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.
@@ -36,6 +36,7 @@ import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigU
*
* @author l burgazzoli
* @author Haytham Mohamed
* @author Isik Erhan
*/
@Order(1)
public class Fabric8SecretsPropertySourceLocator extends SecretsPropertySourceLocator {
@@ -72,7 +73,8 @@ public class Fabric8SecretsPropertySourceLocator extends SecretsPropertySourceLo
String secretNamespace = getApplicationNamespace(this.client, normalizedSource.getNamespace(),
configurationTarget, provider);
Map<String, String> labels = normalizedSource.getLabels();
return new Fabric8SecretsPropertySource(this.client, secretName, secretNamespace, labels);
return new Fabric8SecretsPropertySource(this.client, secretName, secretNamespace, labels,
this.properties.isFailFast());
}
}

View File

@@ -0,0 +1,226 @@
/*
* 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.fabric8.config;
import java.util.HashMap;
import java.util.Map;
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.context.ApplicationContext;
import org.springframework.core.env.PropertySource;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* @author Isik Erhan
*/
@EnableKubernetesMockClient
public class Fabric8ConfigMapPropertySourceLocatorRetryTests {
private static final String API = "/api/v1/namespaces/default/configmaps/application";
static KubernetesMockServer mockServer;
static KubernetesClient mockClient;
@BeforeAll
public static void setup() {
// Configure the kubernetes master url to point to the mock server
System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, mockClient.getConfiguration().getMasterUrl());
System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true");
System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false");
System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false");
System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true");
}
@Nested
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
properties = { "spring.cloud.kubernetes.client.namespace=default",
"spring.cloud.kubernetes.config.fail-fast=true",
"spring.cloud.kubernetes.config.retry.max-attempts=5" },
classes = Application.class)
@EnableKubernetesMockClient
class ConfigRetryEnabled {
@SpyBean
private Fabric8ConfigMapPropertySourceLocator propertySourceLocator;
@Test
public void locateShouldNotRetryWhenThereIsNoFailure() {
Map<String, String> data = new HashMap<>();
data.put("some.prop", "theValue");
data.put("some.number", "0");
// return config map without failing
mockServer.expect().withPath(API).andReturn(200, new ConfigMapBuilder().withNewMetadata()
.withName("application").endMetadata().addToData(data).build()).once();
PropertySource<?> propertySource = Assertions
.assertDoesNotThrow(() -> propertySourceLocator.locate(new MockEnvironment()));
// verify locate is called only once
verify(propertySourceLocator, times(1)).locate(any());
// validate the contents of the property source
assertThat(propertySource.getProperty("some.prop")).isEqualTo("theValue");
assertThat(propertySource.getProperty("some.number")).isEqualTo("0");
}
@Test
public void locateShouldRetryAndRecover() {
Map<String, String> data = new HashMap<>();
data.put("some.prop", "theValue");
data.put("some.number", "0");
// fail 3 times then succeed at the 4th call
mockServer.expect().withPath(API).andReturn(500, "Internal Server Error").times(3);
mockServer.expect().withPath(API).andReturn(200, new ConfigMapBuilder().withNewMetadata()
.withName("application").endMetadata().addToData(data).build()).once();
PropertySource<?> propertySource = Assertions
.assertDoesNotThrow(() -> propertySourceLocator.locate(new MockEnvironment()));
// verify retried 4 times
verify(propertySourceLocator, times(4)).locate(any());
// validate the contents of the property source
assertThat(propertySource.getProperty("some.prop")).isEqualTo("theValue");
assertThat(propertySource.getProperty("some.number")).isEqualTo("0");
}
@Test
public void locateShouldRetryAndFail() {
// fail all the 5 requests
mockServer.expect().withPath(API).andReturn(500, "Internal Server Error").times(5);
assertThatThrownBy(() -> propertySourceLocator.locate(new MockEnvironment()))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read ConfigMap with name 'application' in namespace 'default'");
// verify retried 5 times until failure
verify(propertySourceLocator, times(5)).locate(any());
}
}
@Nested
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
properties = { "spring.cloud.kubernetes.client.namespace=default" }, classes = Application.class)
@EnableKubernetesMockClient
class ConfigFailFastDisabled {
@SpyBean
private Fabric8ConfigMapPropertySourceLocator propertySourceLocator;
@Test
public void locateShouldNotRetry() {
mockServer.expect().withPath(API).andReturn(500, "Internal Server Error").once();
Assertions.assertDoesNotThrow(() -> propertySourceLocator.locate(new MockEnvironment()));
// verify that propertySourceLocator.locate is called only once
verify(propertySourceLocator, times(1)).locate(any());
}
}
@Nested
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, properties = {
"spring.cloud.kubernetes.client.namespace=default", "spring.cloud.kubernetes.config.fail-fast=true",
"spring.cloud.kubernetes.config.retry.enabled=false", "spring.cloud.kubernetes.secrets.fail-fast=true" },
classes = Application.class)
@EnableKubernetesMockClient
class ConfigRetryDisabledButSecretsRetryEnabled {
@SpyBean
private Fabric8ConfigMapPropertySourceLocator propertySourceLocator;
@Autowired
private ApplicationContext context;
@Test
public void locateShouldFailWithoutRetrying() {
/*
* Enabling secrets retry causes Spring Retry to be enabled and a
* RetryOperationsInterceptor bean with NeverRetryPolicy for config maps to be
* defined. ConfigMapPropertySourceLocator should not retry even Spring Retry
* is enabled.
*/
mockServer.expect().withPath(API).andReturn(500, "Internal Server Error").once();
assertThat(context.containsBean("kubernetesConfigRetryInterceptor")).isTrue();
assertThatThrownBy(() -> propertySourceLocator.locate(new MockEnvironment()))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read ConfigMap with name 'application' in namespace 'default'");
// verify that propertySourceLocator.locate is called only once
verify(propertySourceLocator, times(1)).locate(any());
}
}
@Nested
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
properties = { "spring.cloud.kubernetes.client.namespace=default",
"spring.cloud.kubernetes.config.fail-fast=true",
"spring.cloud.kubernetes.config.retry.enabled=false" },
classes = Application.class)
@EnableKubernetesMockClient
class ConfigFailFastEnabledButRetryDisabled {
@SpyBean
private Fabric8ConfigMapPropertySourceLocator propertySourceLocator;
@Autowired
private ApplicationContext context;
@Test
public void locateShouldFailWithoutRetrying() {
mockServer.expect().withPath(API).andReturn(500, "Internal Server Error").once();
assertThat(context.containsBean("kubernetesConfigRetryInterceptor")).isFalse();
assertThatThrownBy(() -> propertySourceLocator.locate(new MockEnvironment()))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read ConfigMap with name 'application' in namespace 'default'");
// verify that propertySourceLocator.locate is called only once
verify(propertySourceLocator, times(1)).locate(any());
}
}
}

View File

@@ -0,0 +1,80 @@
/*
* 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.fabric8.config;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* @author Isik Erhan
*/
@EnableKubernetesMockClient
public class Fabric8ConfigMapPropertySourceLocatorTests {
KubernetesMockServer mockServer;
KubernetesClient mockClient;
@Test
public void locateShouldThrowExceptionOnFailureWhenFailFastIsEnabled() {
final String name = "my-config";
final String namespace = "default";
final String path = String.format("/api/v1/namespaces/%s/configmaps/%s", namespace, name);
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties();
configMapConfigProperties.setName(name);
configMapConfigProperties.setNamespace(namespace);
configMapConfigProperties.setFailFast(true);
Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient,
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
assertThatThrownBy(() -> locator.locate(new MockEnvironment())).isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read ConfigMap with name '" + name + "' in namespace '" + namespace + "'");
}
@Test
public void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() {
final String name = "my-config";
final String namespace = "default";
final String path = String.format("/api/v1/namespaces/%s/configmaps/%s", namespace, name);
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties();
configMapConfigProperties.setName(name);
configMapConfigProperties.setNamespace(namespace);
configMapConfigProperties.setFailFast(false);
Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient,
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
assertThatNoException().isThrownBy(() -> locator.locate(new MockEnvironment()));
}
}

View File

@@ -0,0 +1,62 @@
/*
* 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.fabric8.config;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer;
import org.junit.jupiter.api.Test;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* @author Isik Erhan
*/
@EnableKubernetesMockClient
public class Fabric8ConfigMapPropertySourceTests {
KubernetesMockServer mockServer;
KubernetesClient mockClient;
@Test
public void constructorShouldThrowExceptionOnFailureWhenFailFastIsEnabled() {
final String name = "my-config";
final String namespace = "default";
final String path = String.format("/api/v1/namespaces/%s/configmaps/%s", namespace, name);
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
assertThatThrownBy(() -> new Fabric8ConfigMapPropertySource(mockClient, name, namespace, new MockEnvironment(),
"", false, true)).isInstanceOf(IllegalStateException.class).hasMessage(
"Unable to read ConfigMap with name '" + name + "' in namespace '" + namespace + "'");
}
@Test
public void constructorShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() {
final String name = "my-config";
final String namespace = "default";
final String path = String.format("/api/v1/namespaces/%s/configmaps/%s", namespace, name);
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
assertThatNoException().isThrownBy(() -> new Fabric8ConfigMapPropertySource(mockClient, name, namespace,
new MockEnvironment(), "", false, false));
}
}

View File

@@ -0,0 +1,237 @@
/*
* 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.fabric8.config;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import io.fabric8.kubernetes.api.model.SecretBuilder;
import io.fabric8.kubernetes.api.model.SecretListBuilder;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.context.ApplicationContext;
import org.springframework.core.env.PropertySource;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* @author Isik Erhan
*/
@EnableKubernetesMockClient
public class Fabric8SecretsPropertySourceLocatorRetryTests {
private static final String API = "/api/v1/namespaces/default/secrets/my-secret";
private static final String LIST_API = "/api/v1/namespaces/default/secrets";
static KubernetesMockServer mockServer;
static KubernetesClient mockClient;
@BeforeAll
public static void setup() {
// Configure the kubernetes master url to point to the mock server
System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, mockClient.getConfiguration().getMasterUrl());
System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true");
System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false");
System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false");
System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true");
// return empty secret list to not fail context creation
mockServer.expect().withPath(LIST_API).andReturn(200, new SecretListBuilder().build()).always();
}
@Nested
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, properties = {
"spring.cloud.kubernetes.client.namespace=default", "spring.cloud.kubernetes.secrets.fail-fast=true",
"spring.cloud.kubernetes.secrets.retry.max-attempts=5", "spring.cloud.kubernetes.secrets.name=my-secret",
"spring.cloud.kubernetes.secrets.enable-api=true" }, classes = Application.class)
@EnableKubernetesMockClient
class SecretsRetryEnabled {
@SpyBean
private Fabric8SecretsPropertySourceLocator propertySourceLocator;
@Test
public void locateShouldNotRetryWhenThereIsNoFailure() {
Map<String, String> data = new HashMap<>();
data.put("some.sensitive.prop", Base64.getEncoder().encodeToString("theSensitiveValue".getBytes()));
data.put("some.sensitive.number", Base64.getEncoder().encodeToString("1".getBytes()));
// return secret without failing
mockServer.expect().withPath(API).andReturn(200,
new SecretBuilder().withNewMetadata().withName("my-secret").endMetadata().addToData(data).build())
.once();
PropertySource<?> propertySource = Assertions
.assertDoesNotThrow(() -> propertySourceLocator.locate(new MockEnvironment()));
// verify locate is called only once
verify(propertySourceLocator, times(1)).locate(any());
// validate the contents of the property source
assertThat(propertySource.getProperty("some.sensitive.prop")).isEqualTo("theSensitiveValue");
assertThat(propertySource.getProperty("some.sensitive.number")).isEqualTo("1");
}
@Test
public void locateShouldRetryAndRecover() {
Map<String, String> data = new HashMap<>();
data.put("some.sensitive.prop", Base64.getEncoder().encodeToString("theSensitiveValue".getBytes()));
data.put("some.sensitive.number", Base64.getEncoder().encodeToString("1".getBytes()));
// fail 3 times then succeed at the 4th call
mockServer.expect().withPath(API).andReturn(500, "Internal Server Error").times(3);
mockServer.expect().withPath(API).andReturn(200,
new SecretBuilder().withNewMetadata().withName("my-secret").endMetadata().addToData(data).build())
.once();
PropertySource<?> propertySource = Assertions
.assertDoesNotThrow(() -> propertySourceLocator.locate(new MockEnvironment()));
// verify retried 4 times
verify(propertySourceLocator, times(4)).locate(any());
// validate the contents of the property source
assertThat(propertySource.getProperty("some.sensitive.prop")).isEqualTo("theSensitiveValue");
assertThat(propertySource.getProperty("some.sensitive.number")).isEqualTo("1");
}
@Test
public void locateShouldRetryAndFail() {
// fail all the 5 requests
mockServer.expect().withPath(API).andReturn(500, "Internal Server Error").times(5);
assertThatThrownBy(() -> propertySourceLocator.locate(new MockEnvironment()))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read Secret with name 'my-secret' or labels [{}] in namespace 'default'");
// verify retried 5 times until failure
verify(propertySourceLocator, times(5)).locate(any());
}
}
@Nested
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
properties = { "spring.cloud.kubernetes.client.namespace=default",
"spring.cloud.kubernetes.secrets.name=my-secret",
"spring.cloud.kubernetes.secrets.enable-api=true" },
classes = Application.class)
@EnableKubernetesMockClient
class SecretsFailFastDisabled {
@SpyBean
private Fabric8SecretsPropertySourceLocator propertySourceLocator;
@Test
public void locateShouldNotRetry() {
mockServer.expect().withPath(API).andReturn(500, "Internal Server Error").once();
Assertions.assertDoesNotThrow(() -> propertySourceLocator.locate(new MockEnvironment()));
// verify locate is called only once
verify(propertySourceLocator, times(1)).locate(any());
}
}
@Nested
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, properties = {
"spring.cloud.kubernetes.client.namespace=default", "spring.cloud.kubernetes.secrets.fail-fast=true",
"spring.cloud.kubernetes.secrets.retry.enabled=false", "spring.cloud.kubernetes.config.fail-fast=true",
"spring.cloud.kubernetes.secrets.name=my-secret", "spring.cloud.kubernetes.secrets.enable-api=true" },
classes = Application.class)
@EnableKubernetesMockClient
class SecretsRetryDisabledButConfigRetryEnabled {
@SpyBean
private Fabric8SecretsPropertySourceLocator propertySourceLocator;
@Autowired
private ApplicationContext context;
@Test
public void locateShouldFailWithoutRetrying() {
/*
* Enabling config retry causes Spring Retry to be enabled and a
* RetryOperationsInterceptor bean with NeverRetryPolicy for secrets to be
* defined. SecretsPropertySourceLocator should not retry even Spring Retry is
* enabled.
*/
mockServer.expect().withPath(API).andReturn(500, "Internal Server Error").once();
assertThat(context.containsBean("kubernetesSecretsRetryInterceptor")).isTrue();
assertThatThrownBy(() -> propertySourceLocator.locate(new MockEnvironment()))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read Secret with name 'my-secret' or labels [{}] in namespace 'default'");
// verify that propertySourceLocator.locate is called only once
verify(propertySourceLocator, times(1)).locate(any());
}
}
@Nested
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, properties = {
"spring.cloud.kubernetes.client.namespace=default", "spring.cloud.kubernetes.secrets.fail-fast=true",
"spring.cloud.kubernetes.secrets.retry.enabled=false", "spring.cloud.kubernetes.secrets.name=my-secret",
"spring.cloud.kubernetes.secrets.enable-api=true" }, classes = Application.class)
@EnableKubernetesMockClient
class SecretsFailFastEnabledButRetryDisabled {
@SpyBean
private Fabric8SecretsPropertySourceLocator propertySourceLocator;
@Autowired
private ApplicationContext context;
@Test
public void locateShouldFailWithoutRetrying() {
mockServer.expect().withPath(API).andReturn(500, "Internal Server Error").once();
assertThat(context.containsBean("kubernetesSecretsRetryInterceptor")).isFalse();
assertThatThrownBy(() -> propertySourceLocator.locate(new MockEnvironment()))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read Secret with name 'my-secret' or labels [{}] in namespace 'default'");
// verify that propertySourceLocator.locate is called only once
verify(propertySourceLocator, times(1)).locate(any());
}
}
}

View File

@@ -0,0 +1,83 @@
/*
* 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.fabric8.config;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* @author Isik Erhan
*/
@EnableKubernetesMockClient
public class Fabric8SecretsPropertySourceLocatorTests {
KubernetesMockServer mockServer;
KubernetesClient mockClient;
@Test
public void locateShouldThrowExceptionOnFailureWhenFailFastIsEnabled() {
final String name = "my-config";
final String namespace = "default";
final String path = String.format("/api/v1/namespaces/%s/secrets/%s", namespace, name);
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
SecretsConfigProperties configMapConfigProperties = new SecretsConfigProperties();
configMapConfigProperties.setName(name);
configMapConfigProperties.setNamespace(namespace);
configMapConfigProperties.setEnableApi(true);
configMapConfigProperties.setFailFast(true);
Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient,
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
assertThatThrownBy(() -> locator.locate(new MockEnvironment())).isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read Secret with name '" + name + "' or labels [{}] in namespace '" + namespace
+ "'");
}
@Test
public void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() {
final String name = "my-config";
final String namespace = "default";
final String path = String.format("/api/v1/namespaces/%s/secrets/%s", namespace, name);
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
SecretsConfigProperties configMapConfigProperties = new SecretsConfigProperties();
configMapConfigProperties.setName(name);
configMapConfigProperties.setNamespace(namespace);
configMapConfigProperties.setEnableApi(true);
configMapConfigProperties.setFailFast(false);
Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient,
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
assertThatNoException().isThrownBy(() -> locator.locate(new MockEnvironment()));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2019 the original author or authors.
* 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.
@@ -17,12 +17,16 @@
package org.springframework.cloud.kubernetes.fabric8.config;
import java.util.Base64;
import java.util.Collections;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.api.model.SecretBuilder;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
import io.fabric8.kubernetes.client.server.mock.KubernetesServer;
import org.junit.ClassRule;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -36,6 +40,8 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import static java.util.Collections.singletonMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = App.class)
@@ -47,6 +53,9 @@ public class Fabric8SecretsPropertySourceTest {
private static KubernetesClient mockClient;
@ClassRule
public static KubernetesServer mockServer = new KubernetesServer(false);
private static final String SECRET_VALUE = "secretValue";
@Autowired
@@ -70,6 +79,13 @@ public class Fabric8SecretsPropertySourceTest {
.withLabels(singletonMap("foo", "bar")).endMetadata()
.addToData("secretName", Base64.getEncoder().encodeToString(SECRET_VALUE.getBytes())).build();
mockClient.secrets().inNamespace(NAMESPACE).create(secret);
mockServer.before();
}
@AfterAll
public static void tearDown() {
mockServer.after();
}
@Test
@@ -79,4 +95,28 @@ public class Fabric8SecretsPropertySourceTest {
assertThat(actual).doesNotContain(SECRET_VALUE);
}
@Test
public void constructorShouldThrowExceptionOnFailureWhenFailFastIsEnabled() {
final String name = "my-config";
final String namespace = "default";
final String path = String.format("/api/v1/namespaces/%s/secrets/%s", namespace, name);
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
assertThatThrownBy(() -> new Fabric8SecretsPropertySource(mockServer.getClient(), name, namespace,
Collections.emptyMap(), true)).isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read Secret with name '" + name + "' or labels [{}] in namespace '"
+ namespace + "'");
}
@Test
public void constructorShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() {
final String name = "my-config";
final String namespace = "default";
final String path = String.format("/api/v1/namespaces/%s/secrets/%s", namespace, name);
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
assertThatNoException().isThrownBy(() -> new Fabric8SecretsPropertySource(mockServer.getClient(), name,
namespace, Collections.emptyMap(), false));
}
}