diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc
index 93198f54e..87777287d 100644
--- a/docs/src/main/asciidoc/_configprops.adoc
+++ b/docs/src/main/asciidoc/_configprops.adoc
@@ -120,6 +120,10 @@
|spring.cloud.vault.rabbitmq.username-property | `spring.rabbitmq.username` | Target property for the obtained username.
|spring.cloud.vault.reactive.enabled | `true` | Flag to indicate that reactive discovery is enabled
|spring.cloud.vault.read-timeout | `15000` | Read timeout.
+|spring.cloud.vault.retry.initial-interval | `1000` | Initial retry interval in milliseconds.
+|spring.cloud.vault.retry.multiplier | `1.1` | Multiplier for next interval.
+|spring.cloud.vault.retry.max-interval | `2000` | Maximum interval for backoff.
+|spring.cloud.vault.retry.max-attempts | `6` | Maximum number of attempts.
|spring.cloud.vault.scheme | `https` | Protocol scheme. Can be either "http" or "https".
|spring.cloud.vault.session.lifecycle.enabled | `true` | Enable session lifecycle management.
|spring.cloud.vault.session.lifecycle.expiry-threshold | `7s` | The expiry threshold for a {@link LoginToken}. The threshold represents a minimum TTL duration to consider a login token as valid. Tokens with a shorter TTL are considered expired and are not used anymore. Should be greater than {@code refreshBeforeExpiry} to prevent token expiry.
@@ -136,4 +140,4 @@
|spring.cloud.vault.token | | Static vault token. Required if {@link #authentication} is {@code TOKEN}.
|spring.cloud.vault.uri | | Vault URI. Can be set with scheme, host and port.
-|===
\ No newline at end of file
+|===
diff --git a/docs/src/main/asciidoc/other-topics.adoc b/docs/src/main/asciidoc/other-topics.adoc
index 67ef84e69..5b6de0657 100644
--- a/docs/src/main/asciidoc/other-topics.adoc
+++ b/docs/src/main/asciidoc/other-topics.adoc
@@ -35,6 +35,15 @@ spring.cloud.vault:
----
====
+[[vault.config.retry]]
+== Vault Client Retry
+
+If you expect that the vault server may occasionally be unavailable when your application starts, you can make it keep trying after a failure.
+First, you need to set `spring.cloud.vault.fail-fast=true`.
+Then you need to add `spring-retry` to your classpath.
+The default behavior is to retry six times with an initial backoff interval of 1000ms and an exponential multiplier of 1.1 for subsequent backoffs.
+You can configure these properties by setting the `spring.cloud.vault.retry.*` configuration properties.
+
[[vault.config.namespaces]]
== Vault Enterprise Namespace Support
diff --git a/spring-cloud-vault-config/pom.xml b/spring-cloud-vault-config/pom.xml
index bc15a7429..4e09fdbb8 100644
--- a/spring-cloud-vault-config/pom.xml
+++ b/spring-cloud-vault-config/pom.xml
@@ -59,6 +59,12 @@
true
+
+ org.springframework.retry
+ spring-retry
+ true
+
+
org.apache.httpcomponents
@@ -187,6 +193,13 @@
junit-vintage-engine
test
+
+
+ org.springframework.cloud
+ spring-cloud-test-support
+ ${spring-cloud-commons.version}
+ test
+
diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/RetryProperties.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/RetryProperties.java
new file mode 100644
index 000000000..240441128
--- /dev/null
+++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/RetryProperties.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2014-2019 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.vault.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+@ConfigurationProperties(RetryProperties.PREFIX)
+public class RetryProperties {
+
+ /**
+ * ConfigurationProperties prefix.
+ */
+ public static final String PREFIX = "spring.cloud.vault.retry";
+
+ public static final Set PROPERTY_SET = new HashSet<>(
+ Arrays.asList("spring.cloud.vault.retry.max-attempts", "spring.cloud.vault.retry.multiplier",
+ "spring.cloud.vault.retry.initial-interval", "spring.cloud.vault.retry.max-interval"));
+
+ /**
+ * Initial retry interval in milliseconds.
+ */
+ long initialInterval = 1000;
+
+ /**
+ * Multiplier for next interval.
+ */
+ double multiplier = 1.1;
+
+ /**
+ * Maximum interval for backoff.
+ */
+ long maxInterval = 2000;
+
+ /**
+ * Maximum number of attempts.
+ */
+ 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-vault-config/src/main/java/org/springframework/cloud/vault/config/RetryableClientHttpRequest.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/RetryableClientHttpRequest.java
new file mode 100644
index 000000000..45a8a6a01
--- /dev/null
+++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/RetryableClientHttpRequest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2016-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.vault.config;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.client.ClientHttpRequest;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.retry.RetryOperations;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+
+/**
+ * {@link ClientHttpRequest} configured with retry support
+ */
+class RetryableClientHttpRequest implements ClientHttpRequest {
+
+ private final ClientHttpRequest delegateRequest;
+
+ private final RetryOperations retryOperations;
+
+ RetryableClientHttpRequest(ClientHttpRequest request, RetryOperations retryOperations) {
+ this.delegateRequest = request;
+ this.retryOperations = retryOperations;
+ }
+
+ @Override
+ public ClientHttpResponse execute() throws IOException {
+ return retryOperations.execute(retryContext -> delegateRequest.execute());
+ }
+
+ @Override
+ public OutputStream getBody() throws IOException {
+ return delegateRequest.getBody();
+ }
+
+ @Override
+ public String getMethodValue() {
+ return delegateRequest.getMethodValue();
+ }
+
+ @Override
+ public URI getURI() {
+ return delegateRequest.getURI();
+ }
+
+ @Override
+ public HttpHeaders getHeaders() {
+ return delegateRequest.getHeaders();
+ }
+
+}
diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultAutoConfiguration.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultAutoConfiguration.java
index fa164dff4..8b3fa3cc9 100644
--- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultAutoConfiguration.java
+++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultAutoConfiguration.java
@@ -19,7 +19,10 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectFactory;
@@ -35,10 +38,13 @@
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.annotation.Order;
+import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.retry.support.RetryTemplate;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
+import org.springframework.util.ClassUtils;
import org.springframework.vault.authentication.ClientAuthentication;
import org.springframework.vault.authentication.LifecycleAwareSessionManager;
import org.springframework.vault.authentication.SessionManager;
@@ -64,14 +70,20 @@
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.vault.enabled", matchIfMissing = true)
-@EnableConfigurationProperties(VaultProperties.class)
+@EnableConfigurationProperties({ VaultProperties.class, RetryProperties.class })
@Order(Ordered.LOWEST_PRECEDENCE - 5)
public class VaultAutoConfiguration {
+ private final Log log = LogFactory.getLog(getClass());
+
+ private static final String RETRY_TEMPLATE = "org.springframework.retry.support.RetryTemplate";
+
private final ConfigurableApplicationContext applicationContext;
private final VaultProperties vaultProperties;
+ private final RetryProperties retryProperties;
+
private final VaultConfiguration configuration;
private final VaultEndpointProvider endpointProvider;
@@ -81,12 +93,13 @@ public class VaultAutoConfiguration {
private final List> requestCustomizers;
public VaultAutoConfiguration(ConfigurableApplicationContext applicationContext, VaultProperties vaultProperties,
- ObjectProvider endpointProvider,
+ RetryProperties retryProperties, ObjectProvider endpointProvider,
ObjectProvider> customizers,
ObjectProvider>> requestCustomizers) {
this.applicationContext = applicationContext;
this.vaultProperties = vaultProperties;
+ this.retryProperties = retryProperties;
this.configuration = new VaultConfiguration(vaultProperties);
VaultEndpointProvider provider = endpointProvider.getIfAvailable();
@@ -129,7 +142,32 @@ protected RestTemplateBuilder restTemplateBuilder(ClientHttpRequestFactory reque
@Bean
@ConditionalOnMissingBean
public ClientFactoryWrapper clientHttpRequestFactoryWrapper() {
- return new ClientFactoryWrapper(this.configuration.createClientHttpRequestFactory());
+ ClientHttpRequestFactory clientHttpRequestFactory = this.configuration.createClientHttpRequestFactory();
+ if (this.vaultProperties.isFailFast()) {
+ if (ClassUtils.isPresent(RETRY_TEMPLATE, getClass().getClassLoader())) {
+ Map beans = applicationContext.getBeansOfType(RetryTemplate.class);
+ if (!beans.isEmpty()) {
+ Map.Entry existingBean = beans.entrySet().stream().findFirst().get();
+ log.info("Using existing RestTemplate '" + existingBean.getKey() + "' for vault retries");
+ clientHttpRequestFactory = VaultRetryUtil
+ .createRetryableClientHttpRequestFactory(existingBean.getValue(), clientHttpRequestFactory);
+ }
+ else {
+ clientHttpRequestFactory = VaultRetryUtil.createRetryableClientHttpRequestFactory(retryProperties,
+ clientHttpRequestFactory);
+ }
+ }
+ else {
+ ConfigurableEnvironment env = applicationContext.getEnvironment();
+ boolean retryPropertySet = RetryProperties.PROPERTY_SET.stream()
+ .anyMatch(s -> env.getProperty(s) != null);
+ if (retryPropertySet) {
+ throw new IllegalStateException(
+ "One or more spring.cloud.vault.retry.* properties are set, but spring-retry is not on the classpath");
+ }
+ }
+ }
+ return new ClientFactoryWrapper(clientHttpRequestFactory);
}
/**
diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapConfiguration.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapConfiguration.java
index de44953b0..dafa4b11b 100644
--- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapConfiguration.java
+++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapConfiguration.java
@@ -38,16 +38,17 @@
* {@code @EnableAutoConfiguration}.
*/
@ConditionalOnProperty(name = "spring.cloud.vault.enabled", matchIfMissing = true)
-@EnableConfigurationProperties(VaultProperties.class)
+@EnableConfigurationProperties({ VaultProperties.class, RetryProperties.class })
@Order(Ordered.LOWEST_PRECEDENCE - 5)
@Deprecated
public class VaultBootstrapConfiguration extends VaultAutoConfiguration {
public VaultBootstrapConfiguration(ConfigurableApplicationContext applicationContext,
- VaultProperties vaultProperties, ObjectProvider endpointProvider,
+ VaultProperties vaultProperties, RetryProperties retryProperties,
+ ObjectProvider endpointProvider,
ObjectProvider> customizers,
ObjectProvider>> requestCustomizers) {
- super(applicationContext, vaultProperties, endpointProvider, customizers, requestCustomizers);
+ super(applicationContext, vaultProperties, retryProperties, endpointProvider, customizers, requestCustomizers);
}
}
diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLoader.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLoader.java
index c3f92c17f..80e473624 100644
--- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLoader.java
+++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLoader.java
@@ -43,9 +43,11 @@
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.lang.Nullable;
+import org.springframework.retry.support.RetryTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
@@ -102,6 +104,9 @@ public class VaultConfigDataLoader implements ConfigDataLoader {
ClientHttpRequestFactory factory = this.configuration.createClientHttpRequestFactory();
+ if (this.vaultProperties.isFailFast()) {
+ if (RETRY_AVAILABLE) {
+ RetryTemplate retryTemplate = bootstrap.getOrElse(RetryTemplate.class,
+ VaultRetryUtil.createRetryTemplate(retryProperties));
+ factory = VaultRetryUtil.createRetryableClientHttpRequestFactory(retryTemplate, factory);
+ }
+ else {
+ Environment env = bootstrap.get(Environment.class);
+ boolean retryPropertySet = RetryProperties.PROPERTY_SET.stream()
+ .anyMatch(s -> env.getProperty(s) != null);
+ if (retryPropertySet) {
+ throw new IllegalStateException(
+ "One or more spring.cloud.vault.retry.* properties are set, but spring-retry is not on the classpath");
+ }
+ }
+ }
// early initialization
try {
diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLocationResolver.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLocationResolver.java
index 5cc02ce14..6206a93d2 100644
--- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLocationResolver.java
+++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLocationResolver.java
@@ -147,6 +147,9 @@ private static void registerVaultProperties(ConfigDataLocationResolverContext co
return vaultProperties;
});
+
+ context.getBootstrapContext().registerIfAbsent(RetryProperties.class,
+ ignore -> context.getBinder().bindOrCreate(RetryProperties.PREFIX, RetryProperties.class));
}
private List getSecretBackends(ConfigDataLocationResolverContext context,
diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultRetryUtil.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultRetryUtil.java
new file mode 100644
index 000000000..81f6ac67a
--- /dev/null
+++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultRetryUtil.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2018-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.vault.config;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.http.client.ClientHttpRequest;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.retry.backoff.ExponentialBackOffPolicy;
+import org.springframework.retry.policy.SimpleRetryPolicy;
+import org.springframework.retry.support.RetryTemplate;
+
+/**
+ * Util class for building objects that rely on spring-retry
+ */
+final class VaultRetryUtil {
+
+ private static final Log log = LogFactory.getLog(VaultRetryUtil.class);
+
+ private VaultRetryUtil() {
+
+ }
+
+ static RetryTemplate createRetryTemplate(RetryProperties retryProperties) {
+ RetryTemplate retryTemplate = new RetryTemplate();
+
+ ExponentialBackOffPolicy policy = new ExponentialBackOffPolicy();
+ policy.setInitialInterval(retryProperties.getInitialInterval());
+ policy.setMultiplier(retryProperties.getMultiplier());
+ policy.setMaxInterval(retryProperties.getMaxInterval());
+
+ retryTemplate.setBackOffPolicy(policy);
+ retryTemplate.setRetryPolicy(new SimpleRetryPolicy(retryProperties.getMaxAttempts()));
+
+ return retryTemplate;
+ }
+
+ static ClientHttpRequestFactory createRetryableClientHttpRequestFactory(RetryTemplate retryTemplate,
+ ClientHttpRequestFactory delegate) {
+ return (uri, httpMethod) -> retryTemplate.execute(retryContext -> {
+ ClientHttpRequest request = delegate.createRequest(uri, httpMethod);
+ return new RetryableClientHttpRequest(request, retryTemplate);
+ });
+ }
+
+ static ClientHttpRequestFactory createRetryableClientHttpRequestFactory(RetryProperties retryProperties,
+ ClientHttpRequestFactory delegate) {
+ RetryTemplate retryTemplate = createRetryTemplate(retryProperties);
+
+ return createRetryableClientHttpRequestFactory(retryTemplate, delegate);
+ }
+
+}
diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/RetryableClientHttpRequestTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/RetryableClientHttpRequestTests.java
new file mode 100644
index 000000000..fb6156c6a
--- /dev/null
+++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/RetryableClientHttpRequestTests.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2016-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.vault.config;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.client.ClientHttpRequest;
+import org.springframework.http.client.ClientHttpRequestFactory;
+
+import java.io.IOException;
+import java.net.SocketTimeoutException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class RetryableClientHttpRequestTests {
+
+ @Test
+ public void shouldRetry() throws URISyntaxException, IOException {
+ ClientHttpRequestFactory delegate = mock(ClientHttpRequestFactory.class);
+ ClientHttpRequest delegateRequest = mock(ClientHttpRequest.class);
+ when(delegateRequest.execute()).thenThrow(new SocketTimeoutException());
+ when(delegate.createRequest(any(), any())).thenReturn(delegateRequest);
+ ClientHttpRequestFactory retryableFactory = VaultRetryUtil
+ .createRetryableClientHttpRequestFactory(new RetryProperties(), delegate);
+ ClientHttpRequest request = retryableFactory.createRequest(new URI("https://spring.io/"), HttpMethod.GET);
+ try {
+ request.execute();
+ }
+ catch (SocketTimeoutException e) {
+ // expected
+ }
+ verify(delegateRequest, times(6)).execute();
+
+ }
+
+}
diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultAutoConfigurationRetryTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultAutoConfigurationRetryTests.java
new file mode 100644
index 000000000..b1d66e42f
--- /dev/null
+++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultAutoConfigurationRetryTests.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2016-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.vault.config;
+
+import org.junit.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.client.ClientHttpRequest;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.vault.config.AbstractVaultConfiguration;
+
+import java.net.URI;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests around retry functionality
+ */
+public class VaultAutoConfigurationRetryTests {
+
+ private ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(VaultAutoConfiguration.class));
+
+ @Test
+ public void shouldNotRetryFailFastMissing() {
+
+ this.contextRunner.withPropertyValues("spring.cloud.vault.kv.enabled=false", "spring.cloud.vault.token=foo")
+ .run(context -> {
+ AbstractVaultConfiguration.ClientFactoryWrapper clientFactoryWrapper = context
+ .getBean(AbstractVaultConfiguration.ClientFactoryWrapper.class);
+ ClientHttpRequestFactory requestFactory = clientFactoryWrapper.getClientHttpRequestFactory();
+ ClientHttpRequest request = requestFactory.createRequest(new URI("https://spring.io/"),
+ HttpMethod.GET);
+ assertThat(request instanceof RetryableClientHttpRequest).isFalse();
+ });
+ }
+
+ @Test
+ public void shouldBeConfiguredToRetry() {
+
+ this.contextRunner.withPropertyValues("spring.cloud.vault.kv.enabled=false",
+ "spring.cloud.vault.fail-fast=true", "spring.cloud.vault.token=foo").run(context -> {
+ AbstractVaultConfiguration.ClientFactoryWrapper clientFactoryWrapper = context
+ .getBean(AbstractVaultConfiguration.ClientFactoryWrapper.class);
+ ClientHttpRequestFactory requestFactory = clientFactoryWrapper.getClientHttpRequestFactory();
+ ClientHttpRequest request = requestFactory.createRequest(new URI("https://spring.io/"),
+ HttpMethod.GET);
+ assertThat(request instanceof RetryableClientHttpRequest).isTrue();
+ });
+ }
+
+}
diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultAutoConfigurationRetryUnavailableTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultAutoConfigurationRetryUnavailableTests.java
new file mode 100644
index 000000000..d2e36d08a
--- /dev/null
+++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultAutoConfigurationRetryUnavailableTests.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2016-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.vault.config;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.BeanCreationException;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.cloud.test.ClassPathExclusions;
+import org.springframework.cloud.test.ModifiedClassPathRunner;
+import org.springframework.core.NestedExceptionUtils;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.client.ClientHttpRequest;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.vault.config.AbstractVaultConfiguration;
+
+import java.net.URI;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+
+/**
+ * Tests without spring-retry on the classpath to be sure there is no hard dependency
+ */
+@RunWith(ModifiedClassPathRunner.class)
+@ClassPathExclusions({ "spring-retry-*.jar" })
+public class VaultAutoConfigurationRetryUnavailableTests {
+
+ private ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(VaultAutoConfiguration.class));
+
+ @Test
+ public void shouldNotBeConfigured() {
+
+ this.contextRunner.withPropertyValues("spring.cloud.vault.kv.enabled=false",
+ "spring.cloud.vault.fail-fast=true", "spring.cloud.vault.token=foo").run(context -> {
+ assertThat(context.containsBean("vaultRetryOperations")).isFalse();
+ AbstractVaultConfiguration.ClientFactoryWrapper clientFactoryWrapper = context
+ .getBean(AbstractVaultConfiguration.ClientFactoryWrapper.class);
+ ClientHttpRequestFactory requestFactory = clientFactoryWrapper.getClientHttpRequestFactory();
+ ClientHttpRequest request = requestFactory.createRequest(new URI("https://spring.io/"),
+ HttpMethod.GET);
+ assertThat(request instanceof RetryableClientHttpRequest).isFalse();
+ });
+ }
+
+ @Test
+ public void shouldThrowErrorIfRetryPropertiesConfigured() {
+
+ try {
+ this.contextRunner
+ .withPropertyValues("spring.cloud.vault.kv.enabled=false", "spring.cloud.vault.fail-fast=true",
+ "spring.cloud.vault.token=foo", "spring.cloud.vault.retry.max-attempts=5")
+ .run(context -> {
+ });
+ }
+ catch (BeanCreationException e) {
+ Throwable cause = NestedExceptionUtils.getRootCause(e);
+ assertThat(cause.getMessage()).contains("spring-retry is not on the classpath");
+ }
+ }
+
+}
diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigLoaderRetryTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigLoaderRetryTests.java
new file mode 100644
index 000000000..f4fa6fbf3
--- /dev/null
+++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigLoaderRetryTests.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2016-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.vault.config;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.DefaultBootstrapContext;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.WebApplicationType;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor;
+import org.springframework.boot.logging.DeferredLog;
+import org.springframework.boot.logging.DeferredLogs;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.env.ConfigurableEnvironment;
+import org.springframework.core.env.MapPropertySource;
+import org.springframework.core.env.StandardEnvironment;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.client.ClientHttpRequest;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.test.context.support.TestPropertySourceUtils;
+import org.springframework.vault.VaultException;
+import org.springframework.vault.config.AbstractVaultConfiguration;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests around retry functionality
+ */
+public class VaultConfigLoaderRetryTests {
+
+ private final Log log = LogFactory.getLog(VaultConfigLoaderRetryTests.class);
+
+ private ApplicationContextRunner contextRunner = new ApplicationContextRunner();
+
+ @Test
+ public void shouldNotRetryFailFastMissing() throws URISyntaxException, IOException {
+ SpringApplication app = new SpringApplication(TestApplication.class);
+ Map properties = TestPropertySourceUtils.convertInlinedPropertiesToMap(
+ "spring.cloud.vault.lifecycle.enabled=false",
+ "spring.cloud.vault.uri=http://serverhostdoesnotexist:1234", "spring.config.import=vault://");
+ app.setDefaultProperties(properties);
+ app.setWebApplicationType(WebApplicationType.NONE);
+
+ ConfigurableApplicationContext context = app.run();
+ AbstractVaultConfiguration.ClientFactoryWrapper clientFactoryWrapper = context
+ .getBean(AbstractVaultConfiguration.ClientFactoryWrapper.class);
+ ClientHttpRequestFactory requestFactory = clientFactoryWrapper.getClientHttpRequestFactory();
+ ClientHttpRequest request = requestFactory.createRequest(new URI("https://spring.io/"), HttpMethod.GET);
+ assertThat(request instanceof RetryableClientHttpRequest).isFalse();
+ }
+
+ @Test
+ public void shouldBeConfiguredToRetry() throws URISyntaxException, IOException {
+ SpringApplication app = new SpringApplication(TestApplication.class);
+ app.setWebApplicationType(WebApplicationType.NONE);
+ DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();
+
+ Map properties = TestPropertySourceUtils.convertInlinedPropertiesToMap(
+ "spring.profiles.active=test", "spring.cloud.vault.fail-fast=true",
+ "spring.cloud.vault.reactive.enabled=false", "spring.cloud.vault.retry.max-attempts=2",
+ "spring.cloud.vault.lifecycle.enabled=false",
+ "spring.cloud.vault.uri=http://serverhostdoesnotexist:1234", "spring.config.import=vault://");
+ ConfigurableEnvironment environment = new StandardEnvironment();
+ environment.getPropertySources().addFirst(new MapPropertySource("testPropertiesSource", properties));
+ DeferredLogs logs = new DeferredLogs();
+ ConfigDataEnvironmentPostProcessor configDataEnvironmentPostProcessor = new ConfigDataEnvironmentPostProcessor(
+ new DeferredLogs(), bootstrapContext);
+ try {
+ configDataEnvironmentPostProcessor.postProcessEnvironment(environment, app);
+ }
+ catch (VaultException ve) {
+ // expected since fail-fast is true
+ }
+ ((DeferredLog) logs.getLog(ConfigDataEnvironmentPostProcessor.class)).replayTo(log);
+
+ AbstractVaultConfiguration.ClientFactoryWrapper clientFactoryWrapper = bootstrapContext
+ .get(AbstractVaultConfiguration.ClientFactoryWrapper.class);
+ ClientHttpRequestFactory requestFactory = clientFactoryWrapper.getClientHttpRequestFactory();
+ ClientHttpRequest request = requestFactory.createRequest(new URI("https://spring.io/"), HttpMethod.GET);
+ assertThat(request instanceof RetryableClientHttpRequest).isTrue();
+ }
+
+ @EnableAutoConfiguration(exclude = RefreshAutoConfiguration.class)
+ public static class TestApplication {
+
+ }
+
+}
diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigLoaderRetryUnavailableTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigLoaderRetryUnavailableTests.java
new file mode 100644
index 000000000..474849361
--- /dev/null
+++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigLoaderRetryUnavailableTests.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2016-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.vault.config;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.DefaultBootstrapContext;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.WebApplicationType;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor;
+import org.springframework.boot.logging.DeferredLog;
+import org.springframework.boot.logging.DeferredLogs;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration;
+import org.springframework.cloud.test.ClassPathExclusions;
+import org.springframework.cloud.test.ModifiedClassPathRunner;
+import org.springframework.core.env.ConfigurableEnvironment;
+import org.springframework.core.env.MapPropertySource;
+import org.springframework.core.env.StandardEnvironment;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.client.ClientHttpRequest;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.test.context.support.TestPropertySourceUtils;
+import org.springframework.vault.VaultException;
+import org.springframework.vault.config.AbstractVaultConfiguration;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests without spring-retry on the classpath to be sure there is no hard dependency
+ */
+@RunWith(ModifiedClassPathRunner.class)
+@ClassPathExclusions({ "spring-retry-*.jar" })
+public class VaultConfigLoaderRetryUnavailableTests {
+
+ private final Log log = LogFactory.getLog(getClass());
+
+ private ApplicationContextRunner contextRunner = new ApplicationContextRunner();
+
+ @Test
+ public void shouldNotBeConfiguredToRetry() throws URISyntaxException, IOException {
+ SpringApplication app = new SpringApplication(TestApplication.class);
+ app.setWebApplicationType(WebApplicationType.NONE);
+ DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();
+
+ Map properties = TestPropertySourceUtils.convertInlinedPropertiesToMap(
+ "spring.profiles.active=test", "spring.cloud.vault.fail-fast=true",
+ "spring.cloud.vault.retry.max-attempts=2", "spring.cloud.vault.lifecycle.enabled=false",
+ "spring.cloud.vault.uri=http://serverhostdoesnotexist:1234", "spring.config.import=vault://");
+ ConfigurableEnvironment environment = new StandardEnvironment();
+ environment.getPropertySources().addFirst(new MapPropertySource("testPropertiesSource", properties));
+ DeferredLogs logs = new DeferredLogs();
+ ConfigDataEnvironmentPostProcessor configDataEnvironmentPostProcessor = new ConfigDataEnvironmentPostProcessor(
+ new DeferredLogs(), bootstrapContext);
+ try {
+ configDataEnvironmentPostProcessor.postProcessEnvironment(environment, app);
+ }
+ catch (VaultException ve) {
+ // expected since fail-fast is true
+ }
+ ((DeferredLog) logs.getLog(ConfigDataEnvironmentPostProcessor.class)).replayTo(log);
+
+ AbstractVaultConfiguration.ClientFactoryWrapper clientFactoryWrapper = bootstrapContext
+ .get(AbstractVaultConfiguration.ClientFactoryWrapper.class);
+ ClientHttpRequestFactory requestFactory = clientFactoryWrapper.getClientHttpRequestFactory();
+ ClientHttpRequest request = requestFactory.createRequest(new URI("https://spring.io/"), HttpMethod.GET);
+ assertThat(request instanceof RetryableClientHttpRequest).isFalse();
+ }
+
+ @EnableAutoConfiguration(exclude = RefreshAutoConfiguration.class)
+ public static class TestApplication {
+
+ }
+
+}
diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultReactiveConfigLoaderTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultReactiveConfigLoaderTests.java
new file mode 100644
index 000000000..a7fa95b85
--- /dev/null
+++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultReactiveConfigLoaderTests.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2016-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.vault.config;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.DefaultBootstrapContext;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.WebApplicationType;
+import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor;
+import org.springframework.boot.logging.DeferredLog;
+import org.springframework.boot.logging.DeferredLogs;
+import org.springframework.core.env.ConfigurableEnvironment;
+import org.springframework.core.env.MapPropertySource;
+import org.springframework.core.env.StandardEnvironment;
+import org.springframework.test.context.support.TestPropertySourceUtils;
+import org.springframework.vault.core.ReactiveVaultTemplate;
+
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+
+public class VaultReactiveConfigLoaderTests {
+
+ private final Log log = LogFactory.getLog(VaultReactiveConfigLoaderTests.class);
+
+ /**
+ * Config loader equivalent of
+ * {@link VaultReactiveBootstrapConfigurationTests#shouldNotConfigureReactiveSupport()}
+ */
+ @Test
+ public void shouldNotConfigureReactiveSupport() {
+ SpringApplication app = new SpringApplication(VaultConfigLoaderRetryTests.TestApplication.class);
+ app.setWebApplicationType(WebApplicationType.NONE);
+ DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();
+
+ Map properties = TestPropertySourceUtils.convertInlinedPropertiesToMap(
+ "spring.profiles.active=test", "spring.cloud.vault.reactive.enabled=false",
+ "spring.cloud.vault.uri=http://serverhostdoesnotexist:1234", "spring.config.import=vault://");
+ ConfigurableEnvironment environment = new StandardEnvironment();
+ environment.getPropertySources().addFirst(new MapPropertySource("testPropertiesSource", properties));
+ DeferredLogs logs = new DeferredLogs();
+ ConfigDataEnvironmentPostProcessor configDataEnvironmentPostProcessor = new ConfigDataEnvironmentPostProcessor(
+ new DeferredLogs(), bootstrapContext);
+ configDataEnvironmentPostProcessor.postProcessEnvironment(environment, app);
+ ((DeferredLog) logs.getLog(ConfigDataEnvironmentPostProcessor.class)).replayTo(log);
+
+ assertThatIllegalStateException().isThrownBy(() -> bootstrapContext.get(ReactiveVaultTemplate.class))
+ .withMessageContaining("has not been registered");
+ }
+
+}