diff --git a/docs/src/main/asciidoc/property-source-config.adoc b/docs/src/main/asciidoc/property-source-config.adoc index e27573bb..591b2522 100644 --- a/docs/src/main/asciidoc/property-source-config.adoc +++ b/docs/src/main/asciidoc/property-source-config.adoc @@ -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 <> to get a better understanding of how the namespace of the application is resolved. +<>; 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 <>. +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: diff --git a/spring-cloud-kubernetes-client-config/pom.xml b/spring-cloud-kubernetes-client-config/pom.xml index 362f812f..a08c420e 100644 --- a/spring-cloud-kubernetes-client-config/pom.xml +++ b/spring-cloud-kubernetes-client-config/pom.xml @@ -99,6 +99,21 @@ test + + org.springframework.retry + spring-retry + test + + + org.springframework.boot + spring-boot-starter-aop + test + + + org.mockito + mockito-inline + test + diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySource.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySource.java index ab8049bc..0fdd0513 100644 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySource.java +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySource.java @@ -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 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 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(); diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocator.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocator.java index 5f1000e9..a35d1677 100644 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocator.java +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocator.java @@ -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()); } } diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySource.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySource.java index 2f0b62af..7e17add9 100644 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySource.java +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySource.java @@ -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 labels) { - super(getSourceName(name, namespace), getSourceData(coreV1Api, environment, name, namespace, labels)); + Environment environment, Map labels, boolean failFast) { + super(getSourceName(name, namespace), getSourceData(coreV1Api, environment, name, namespace, labels, failFast)); } private static Map getSourceData(CoreV1Api api, Environment env, String name, String namespace, - Map labels) { + Map labels, boolean failFast) { Map 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); } diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocator.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocator.java index c1b296ba..333730c6 100644 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocator.java +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocator.java @@ -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()); } } diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocatorRetryTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocatorRetryTests.java new file mode 100644 index 00000000..cef67d4f --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocatorRetryTests.java @@ -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 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 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 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 { + + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocatorTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocatorTests.java index 47bd1810..1f18984b 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocatorTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocatorTests.java @@ -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())); + } + } diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceTests.java index 142f5ef0..1bbd563e 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceTests.java @@ -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"))); + } + } diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocatorRetryTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocatorRetryTests.java new file mode 100644 index 00000000..fd4dff71 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocatorRetryTests.java @@ -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 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 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 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 { + + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocatorTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocatorTests.java index d65cd2a2..17149285 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocatorTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocatorTests.java @@ -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())); + } + } diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceTests.java index d94ce47c..cea11b48 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceTests.java @@ -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 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))); + } + } diff --git a/spring-cloud-kubernetes-commons/pom.xml b/spring-cloud-kubernetes-commons/pom.xml index b5e0448f..4f12ac1e 100644 --- a/spring-cloud-kubernetes-commons/pom.xml +++ b/spring-cloud-kubernetes-commons/pom.xml @@ -57,6 +57,17 @@ spring-boot-starter-logging true + + org.springframework.retry + spring-retry + true + + + org.springframework.boot + spring-boot-starter-aop + true + + org.springframework.boot spring-boot-starter-test diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/AbstractConfigProperties.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/AbstractConfigProperties.java index 8538d4d6..d2258310 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/AbstractConfigProperties.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/AbstractConfigProperties.java @@ -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; + } + + } + } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConditionalOnKubernetesConfigFailFastEnabled.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConditionalOnKubernetesConfigFailFastEnabled.java new file mode 100644 index 00000000..96493a40 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConditionalOnKubernetesConfigFailFastEnabled.java @@ -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 + * spring.cloud.kubernetes.config.fail-fast. + * + * @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 { + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConditionalOnKubernetesConfigOrSecretsRetryEnabled.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConditionalOnKubernetesConfigOrSecretsRetryEnabled.java new file mode 100644 index 00000000..5734dd35 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConditionalOnKubernetesConfigOrSecretsRetryEnabled.java @@ -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 { + + } + + } + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConditionalOnKubernetesConfigRetryDisabled.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConditionalOnKubernetesConfigRetryDisabled.java new file mode 100644 index 00000000..b3f4a566 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConditionalOnKubernetesConfigRetryDisabled.java @@ -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 { + + } + + } + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConditionalOnKubernetesConfigRetryEnabled.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConditionalOnKubernetesConfigRetryEnabled.java new file mode 100644 index 00000000..40553ab1 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConditionalOnKubernetesConfigRetryEnabled.java @@ -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 { + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConditionalOnKubernetesSecretsFailFastEnabled.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConditionalOnKubernetesSecretsFailFastEnabled.java new file mode 100644 index 00000000..1ebbffc0 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConditionalOnKubernetesSecretsFailFastEnabled.java @@ -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 + * spring.cloud.kubernetes.secrets.fail-fast. + * + * @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 { + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConditionalOnKubernetesSecretsRetryDisabled.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConditionalOnKubernetesSecretsRetryDisabled.java new file mode 100644 index 00000000..fc9a0840 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConditionalOnKubernetesSecretsRetryDisabled.java @@ -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 { + + } + + } + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConditionalOnKubernetesSecretsRetryEnabled.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConditionalOnKubernetesSecretsRetryEnabled.java new file mode 100644 index 00000000..41ab7e5c --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConditionalOnKubernetesSecretsRetryEnabled.java @@ -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 { + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigProperties.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigProperties.java index 52eba219..1be1f2b9 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigProperties.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigProperties.java @@ -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; diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapPropertySourceLocator.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapPropertySourceLocator.java index 46d69aea..3f54243a 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapPropertySourceLocator.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapPropertySourceLocator.java @@ -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> locateCollection(Environment environment) { + return PropertySourceLocator.super.locateCollection(environment); + } + private MapPropertySource getMapPropertySourceForSingleConfigMap(ConfigurableEnvironment environment, NormalizedSource normalizedSource) { diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesBootstrapConfiguration.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesBootstrapConfiguration.java index 71bb65c0..8e2a972d 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesBootstrapConfiguration.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesBootstrapConfiguration.java @@ -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(); + } + + } + } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsConfigProperties.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsConfigProperties.java index 0d6beb29..2222b1f7 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsConfigProperties.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsConfigProperties.java @@ -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 labels = Collections.emptyMap(); diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java index e1cc97f4..ee07aca3 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java @@ -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> locateCollection(Environment environment) { + return PropertySourceLocator.super.locateCollection(environment); + } + private MapPropertySource getMapPropertySourceForSingleSecret(ConfigurableEnvironment environment, SecretsConfigProperties.NormalizedSource normalizedSource) { diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/KubernetesBootstrapConfigurationTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/KubernetesBootstrapConfigurationTests.java new file mode 100644 index 00000000..1d41e7f0 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/KubernetesBootstrapConfigurationTests.java @@ -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 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 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 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 { + + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerAutoConfiguration.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerAutoConfiguration.java index 3fc7af42..038fac81 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerAutoConfiguration.java +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerAutoConfiguration.java @@ -69,8 +69,8 @@ public class KubernetesConfigServerAutoConfiguration { return (coreApi, applicationName, namespace, springEnv) -> { List namespaces = namespaceSplitter(properties.getSecretsNamespaces(), namespace); List 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 namespaces = namespaceSplitter(properties.getSecretsNamespaces(), namespace); List propertySources = new ArrayList<>(); namespaces.forEach(space -> propertySources.add(new KubernetesClientSecretsPropertySource(coreApi, - applicationName, space, springEnv, new HashMap<>()))); + applicationName, space, springEnv, new HashMap<>(), false))); return propertySources; }; } diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryTests.java index 206c340b..7feb0df3 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryTests.java +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryTests.java @@ -97,15 +97,15 @@ class KubernetesEnvironmentRepositoryTests { kubernetesPropertySourceSuppliers.add((coreApi, applicationName, namespace, springEnv) -> { List 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 propertySources = new ArrayList<>(); propertySources.add(new KubernetesClientSecretsPropertySource(coreApi, applicationName, "default", - springEnv, new HashMap<>())); + springEnv, new HashMap<>(), false)); return propertySources; }); } diff --git a/spring-cloud-kubernetes-dependencies/pom.xml b/spring-cloud-kubernetes-dependencies/pom.xml index cb028fa0..2e419b1c 100644 --- a/spring-cloud-kubernetes-dependencies/pom.xml +++ b/spring-cloud-kubernetes-dependencies/pom.xml @@ -40,6 +40,7 @@ 1.7.7.1 0.1.2 2.26.3 + 1.3.1 @@ -73,6 +74,12 @@ ${istio-client.version} + + org.springframework.retry + spring-retry + ${spring-retry.version} + + org.springframework.cloud diff --git a/spring-cloud-kubernetes-fabric8-config/pom.xml b/spring-cloud-kubernetes-fabric8-config/pom.xml index 518b5f8f..6e00e3df 100644 --- a/spring-cloud-kubernetes-fabric8-config/pom.xml +++ b/spring-cloud-kubernetes-fabric8-config/pom.xml @@ -179,6 +179,17 @@ ${groovy.version} test + + + org.springframework.retry + spring-retry + test + + + org.springframework.boot + spring-boot-starter-aop + test + diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySource.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySource.java index 95629245..f45b8894 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySource.java +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySource.java @@ -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 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 data = getConfigMapData(client, namespace, name); Map 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); } diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocator.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocator.java index f9614e84..21c6b79a 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocator.java +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocator.java @@ -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()); } } diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySource.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySource.java index 32f1e570..3fcf06a8 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySource.java +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySource.java @@ -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 labels) { - super(getSourceName(name, namespace), getSourceData(client, name, namespace, labels)); + Map labels, boolean failFast) { + super(getSourceName(name, namespace), getSourceData(client, name, namespace, labels, failFast)); } private static Map getSourceData(KubernetesClient client, String name, String namespace, - Map labels) { + Map labels, boolean failFast) { Map 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"); } diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceLocator.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceLocator.java index 20cab90f..7f55e4e3 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceLocator.java +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceLocator.java @@ -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 labels = normalizedSource.getLabels(); - return new Fabric8SecretsPropertySource(this.client, secretName, secretNamespace, labels); + return new Fabric8SecretsPropertySource(this.client, secretName, secretNamespace, labels, + this.properties.isFailFast()); } } diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocatorRetryTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocatorRetryTests.java new file mode 100644 index 00000000..51776922 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocatorRetryTests.java @@ -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 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 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()); + } + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocatorTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocatorTests.java new file mode 100644 index 00000000..334d1706 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocatorTests.java @@ -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())); + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceTests.java new file mode 100644 index 00000000..caeb7f78 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceTests.java @@ -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)); + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceLocatorRetryTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceLocatorRetryTests.java new file mode 100644 index 00000000..1751485d --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceLocatorRetryTests.java @@ -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 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 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()); + } + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceLocatorTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceLocatorTests.java new file mode 100644 index 00000000..63eb8ee5 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceLocatorTests.java @@ -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())); + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceTest.java index dbaa5e47..86afbf7d 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceTest.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceTest.java @@ -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)); + } + }