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:
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user