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"); + } + +}