diff --git a/docs/src/main/asciidoc/new-features.adoc b/docs/src/main/asciidoc/new-features.adoc index 11792083..ede4b102 100644 --- a/docs/src/main/asciidoc/new-features.adoc +++ b/docs/src/main/asciidoc/new-features.adoc @@ -8,3 +8,4 @@ This section briefly covers items that are new and noteworthy in the latest rele * Migration of `PropertySource` initialization from Spring Cloud's Bootstrap Context to Spring Boot's <>. * Support for the <> backend. * Configuration of keystore/truststore types through `spring.cloud.vault.ssl.key-store-type=…`/`spring.cloud.vault.ssl.trust-store-type=…` including PEM support. +* Support for `ReactiveDiscoveryClient` by configuring a `ReactiveVaultEndpointProvider`. diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/DiscoveryClientVaultBootstrapConfiguration.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/DiscoveryClientVaultBootstrapConfiguration.java index 0925544b..e429390e 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/DiscoveryClientVaultBootstrapConfiguration.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/DiscoveryClientVaultBootstrapConfiguration.java @@ -16,8 +16,6 @@ package org.springframework.cloud.vault.config; -import java.net.URI; - import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -30,8 +28,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; -import org.springframework.util.StringUtils; -import org.springframework.vault.client.VaultEndpoint; import org.springframework.vault.client.VaultEndpointProvider; /** @@ -51,8 +47,11 @@ public class DiscoveryClientVaultBootstrapConfiguration { private final VaultProperties vaultProperties; + private final VaultConfiguration configuration; + public DiscoveryClientVaultBootstrapConfiguration(VaultProperties vaultProperties) { this.vaultProperties = vaultProperties; + this.configuration = new VaultConfiguration(vaultProperties); } @Bean @@ -67,28 +66,13 @@ public class DiscoveryClientVaultBootstrapConfiguration { @ConditionalOnProperty(name = "spring.cloud.vault.enabled", matchIfMissing = true) public VaultEndpointProvider vaultEndpointProvider(VaultServiceInstanceProvider instanceProvider) { - String serviceId = this.vaultProperties.getDiscovery().getServiceId(); - String fallbackScheme; + return () -> { - if (StringUtils.hasText(this.vaultProperties.getUri())) { - fallbackScheme = URI.create(this.vaultProperties.getUri()).getScheme(); - } - else { - fallbackScheme = this.vaultProperties.getScheme(); - } + String serviceId = this.vaultProperties.getDiscovery().getServiceId(); + ServiceInstance server = instanceProvider.getVaultServerInstance(serviceId); - ServiceInstance server = instanceProvider.getVaultServerInstance(serviceId); - - VaultEndpoint vaultEndpoint = VaultEndpoint.create(server.getHost(), server.getPort()); - - if (server.getMetadata().containsKey("scheme")) { - vaultEndpoint.setScheme(server.getMetadata().get("scheme")); - } - else { - vaultEndpoint.setScheme(server.isSecure() ? "https" : fallbackScheme); - } - - return () -> vaultEndpoint; + return this.configuration.createVaultEndpoint(server); + }; } } diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/ReactiveDiscoveryClientVaultBootstrapConfiguration.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/ReactiveDiscoveryClientVaultBootstrapConfiguration.java new file mode 100644 index 00000000..996ab74d --- /dev/null +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/ReactiveDiscoveryClientVaultBootstrapConfiguration.java @@ -0,0 +1,93 @@ +/* + * Copyright 2017-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 reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; +import org.springframework.cloud.commons.util.UtilAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.vault.client.ReactiveVaultEndpointProvider; +import org.springframework.vault.client.VaultEndpointProvider; +import org.springframework.vault.core.ReactiveVaultOperations; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * {@link org.springframework.cloud.bootstrap.BootstrapConfiguration} providing a + * {@link VaultEndpointProvider} using {@link DiscoveryClient}. + * + * @author Mark Paluch + * @since 1.1 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnProperty("spring.cloud.vault.discovery.enabled") +@ConditionalOnExpression("${spring.cloud.vault.reactive.enabled:true}") +@ConditionalOnClass({ Flux.class, WebClient.class, ReactiveVaultOperations.class, ReactiveDiscoveryClient.class }) +@EnableConfigurationProperties(VaultProperties.class) +@Order(Ordered.LOWEST_PRECEDENCE - 5) +@Import(UtilAutoConfiguration.class) +public class ReactiveDiscoveryClientVaultBootstrapConfiguration { + + private final VaultProperties vaultProperties; + + private final VaultConfiguration configuration; + + public ReactiveDiscoveryClientVaultBootstrapConfiguration(VaultProperties vaultProperties) { + this.vaultProperties = vaultProperties; + this.configuration = new VaultConfiguration(vaultProperties); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(name = "spring.cloud.vault.enabled", matchIfMissing = true) + public ReactiveVaultEndpointProvider reactiveVaultEndpointProvider( + ObjectProvider reactiveDiscoveryClients, + ObjectProvider endpointProviders) { + + ReactiveDiscoveryClient reactiveDiscoveryClient = reactiveDiscoveryClients.getIfAvailable(); + + if (reactiveDiscoveryClient != null) { + ReacvtiveDiscoveryClientVaultServiceInstanceProvider instanceProvider = new ReacvtiveDiscoveryClientVaultServiceInstanceProvider( + reactiveDiscoveryClient); + + return () -> Mono.defer(() -> { + + String serviceId = this.vaultProperties.getDiscovery().getServiceId(); + + return instanceProvider.getVaultServerInstance(serviceId).map(this.configuration::createVaultEndpoint); + }); + } + + VaultEndpointProvider endpointProvider = endpointProviders.getObject(); + + return () -> Mono.fromSupplier(endpointProvider::getVaultEndpoint).subscribeOn(Schedulers.boundedElastic()); + } + +} diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/ReacvtiveDiscoveryClientVaultServiceInstanceProvider.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/ReacvtiveDiscoveryClientVaultServiceInstanceProvider.java new file mode 100644 index 00000000..76af8c41 --- /dev/null +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/ReacvtiveDiscoveryClientVaultServiceInstanceProvider.java @@ -0,0 +1,61 @@ +/* + * Copyright 2018-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.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Mono; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; + +/** + * Provider for {@link ServiceInstance} to look up the Vault service. + * + * @author Mark Paluch + * @since 3.0 + */ +class ReacvtiveDiscoveryClientVaultServiceInstanceProvider { + + private static final Log log = LogFactory.getLog(ReacvtiveDiscoveryClientVaultServiceInstanceProvider.class); + + private final ReactiveDiscoveryClient client; + + ReacvtiveDiscoveryClientVaultServiceInstanceProvider(ReactiveDiscoveryClient client) { + this.client = client; + } + + Mono getVaultServerInstance(String serviceId) { + + log.debug("Locating Vault server (" + serviceId + ") via discovery"); + + return this.client.getInstances(serviceId).collectList().handle((instances, sink) -> { + + if (instances.isEmpty()) { + sink.error(new IllegalStateException("No instances found of Vault server (" + serviceId + ")")); + return; + } + + ServiceInstance instance = instances.get(0); + + log.debug("Located Vault server (" + serviceId + ") via discovery: " + instance); + + sink.next(instance); + }); + } + +} diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfiguration.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfiguration.java index ff20ef6b..1df81b92 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfiguration.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfiguration.java @@ -21,6 +21,7 @@ import java.time.Duration; import java.util.List; import java.util.function.Supplier; +import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.vault.config.VaultProperties.Ssl; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.scheduling.TaskScheduler; @@ -128,6 +129,27 @@ final class VaultConfiguration { return vaultEndpoint; } + VaultEndpoint createVaultEndpoint(ServiceInstance server) { + String fallbackScheme; + + if (StringUtils.hasText(this.vaultProperties.getUri())) { + fallbackScheme = URI.create(this.vaultProperties.getUri()).getScheme(); + } + else { + fallbackScheme = this.vaultProperties.getScheme(); + } + + VaultEndpoint vaultEndpoint = VaultEndpoint.create(server.getHost(), server.getPort()); + + if (server.getMetadata().containsKey("scheme")) { + vaultEndpoint.setScheme(server.getMetadata().get("scheme")); + } + else { + vaultEndpoint.setScheme(server.isSecure() ? "https" : fallbackScheme); + } + return vaultEndpoint; + } + RestTemplateBuilder createRestTemplateBuilder(ClientHttpRequestFactory requestFactory, VaultEndpointProvider endpointProvider, List customizers, List> requestCustomizers) { diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveAutoConfiguration.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveAutoConfiguration.java index 9fd3e972..5f0e5edc 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveAutoConfiguration.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveAutoConfiguration.java @@ -47,6 +47,7 @@ import org.springframework.vault.authentication.ReactiveLifecycleAwareSessionMan import org.springframework.vault.authentication.ReactiveSessionManager; import org.springframework.vault.authentication.SessionManager; import org.springframework.vault.authentication.VaultTokenSupplier; +import org.springframework.vault.client.ReactiveVaultEndpointProvider; import org.springframework.vault.client.SimpleVaultEndpointProvider; import org.springframework.vault.client.VaultEndpointProvider; import org.springframework.vault.client.WebClientBuilder; @@ -82,6 +83,10 @@ public class VaultReactiveAutoConfiguration implements InitializingBean { private final VaultReactiveConfiguration configuration; + @Nullable + private final ReactiveVaultEndpointProvider reactiveEndpointProvider; + + @Nullable private final VaultEndpointProvider endpointProvider; private final List customizers; @@ -96,14 +101,23 @@ public class VaultReactiveAutoConfiguration implements InitializingBean { private WebClientBuilder webClientBuilder; public VaultReactiveAutoConfiguration(VaultProperties vaultProperties, + ObjectProvider reactiveEndpointProvider, ObjectProvider endpointProvider, ObjectProvider> webClientCustomizers) { this.vaultProperties = vaultProperties; this.configuration = new VaultReactiveConfiguration(vaultProperties); - this.endpointProvider = endpointProvider.getIfAvailable( - () -> SimpleVaultEndpointProvider.of(new VaultConfiguration(vaultProperties).createVaultEndpoint())); + this.reactiveEndpointProvider = reactiveEndpointProvider.getIfAvailable(); + + if (this.reactiveEndpointProvider == null) { + this.endpointProvider = endpointProvider.getIfAvailable(() -> SimpleVaultEndpointProvider + .of(new VaultConfiguration(vaultProperties).createVaultEndpoint())); + } + else { + this.endpointProvider = null; + } + this.customizers = new ArrayList<>(webClientCustomizers.getIfAvailable(Collections::emptyList)); AnnotationAwareOrderComparator.sort(this.customizers); } @@ -117,7 +131,18 @@ public class VaultReactiveAutoConfiguration implements InitializingBean { } protected WebClientBuilder webClientBuilder(ClientHttpConnector connector) { - return this.configuration.createWebClientBuilder(connector, this.endpointProvider, this.customizers); + + if (this.reactiveEndpointProvider != null) { + return this.configuration.createWebClientBuilder(connector, this.reactiveEndpointProvider, + this.customizers); + } + + if (this.endpointProvider != null) { + return this.configuration.createWebClientBuilder(connector, this.endpointProvider, this.customizers); + } + + throw new IllegalStateException( + "Cannot create WebClientBuilder as neither ReactiveEndpointProvider nor EndpointProvider configured"); } /** diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveBootstrapConfiguration.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveBootstrapConfiguration.java index 716eb342..656f1ab2 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveBootstrapConfiguration.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveBootstrapConfiguration.java @@ -27,6 +27,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.bootstrap.BootstrapConfiguration; +import org.springframework.vault.client.ReactiveVaultEndpointProvider; import org.springframework.vault.client.VaultEndpointProvider; import org.springframework.vault.client.WebClientCustomizer; import org.springframework.vault.core.ReactiveVaultOperations; @@ -53,9 +54,10 @@ import org.springframework.web.reactive.function.client.WebClient; public class VaultReactiveBootstrapConfiguration extends VaultReactiveAutoConfiguration { public VaultReactiveBootstrapConfiguration(VaultProperties vaultProperties, + ObjectProvider reactiveEndpointProvider, ObjectProvider endpointProvider, ObjectProvider> webClientCustomizers) { - super(vaultProperties, endpointProvider, webClientCustomizers); + super(vaultProperties, reactiveEndpointProvider, endpointProvider, webClientCustomizers); } } diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveConfiguration.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveConfiguration.java index 7a9de33f..74451591 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveConfiguration.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveConfiguration.java @@ -36,6 +36,7 @@ import org.springframework.vault.authentication.SessionManager; import org.springframework.vault.authentication.TokenAuthentication; import org.springframework.vault.authentication.VaultTokenSupplier; import org.springframework.vault.client.ClientHttpConnectorFactory; +import org.springframework.vault.client.ReactiveVaultEndpointProvider; import org.springframework.vault.client.VaultEndpointProvider; import org.springframework.vault.client.VaultHttpHeaders; import org.springframework.vault.client.WebClientBuilder; @@ -70,12 +71,25 @@ final class VaultReactiveConfiguration { return ClientHttpConnectorFactory.create(clientOptions, sslConfiguration); } + WebClientBuilder createWebClientBuilder(ClientHttpConnector connector, + ReactiveVaultEndpointProvider endpointProvider, List customizers) { + + WebClientBuilder builder = WebClientBuilder.builder().httpConnector(connector) + .endpointProvider(endpointProvider); + + return applyCustomizer(customizers, builder); + } + WebClientBuilder createWebClientBuilder(ClientHttpConnector connector, VaultEndpointProvider endpointProvider, List customizers) { WebClientBuilder builder = WebClientBuilder.builder().httpConnector(connector) .endpointProvider(endpointProvider); + return applyCustomizer(customizers, builder); + } + + private WebClientBuilder applyCustomizer(List customizers, WebClientBuilder builder) { customizers.forEach(builder::customizers); if (StringUtils.hasText(this.vaultProperties.getNamespace())) { diff --git a/spring-cloud-vault-config/src/main/resources/META-INF/spring.factories b/spring-cloud-vault-config/src/main/resources/META-INF/spring.factories index 7180e1e9..5539a227 100644 --- a/spring-cloud-vault-config/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-vault-config/src/main/resources/META-INF/spring.factories @@ -6,6 +6,7 @@ org.springframework.cloud.vault.config.VaultHealthIndicatorAutoConfiguration # Bootstrap Configuration org.springframework.cloud.bootstrap.BootstrapConfiguration=\ org.springframework.cloud.vault.config.DiscoveryClientVaultBootstrapConfiguration,\ +org.springframework.cloud.vault.config.ReactiveDiscoveryClientVaultBootstrapConfiguration,\ org.springframework.cloud.vault.config.VaultBootstrapConfiguration,\ org.springframework.cloud.vault.config.VaultReactiveBootstrapConfiguration,\ org.springframework.cloud.vault.config.VaultBootstrapPropertySourceConfiguration diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/ReactiveDiscoveryClientVaultBootstrapConfigurationTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/ReactiveDiscoveryClientVaultBootstrapConfigurationTests.java new file mode 100644 index 00000000..80e598a4 --- /dev/null +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/ReactiveDiscoveryClientVaultBootstrapConfigurationTests.java @@ -0,0 +1,221 @@ +/* + * Copyright 2018-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 java.net.URI; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.Test; +import org.mockito.Mockito; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.vault.client.ReactiveVaultEndpointProvider; +import org.springframework.vault.client.VaultEndpoint; +import org.springframework.vault.client.VaultEndpointProvider; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link ReactiveDiscoveryClientVaultBootstrapConfiguration}. + * + * @author Mark Paluch + */ +public class ReactiveDiscoveryClientVaultBootstrapConfigurationTests { + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(AutoConfigurations + .of(ReactiveDiscoveryClientVaultBootstrapConfiguration.class, VaultBootstrapConfiguration.class)); + + @Test + public void shouldRegisterDefaultBeans() { + + this.contextRunner.withUserConfiguration(ReactiveDiscoveryConfiguration.class) + .withPropertyValues("spring.cloud.vault.token=foo", "spring.cloud.vault.discovery.enabled=true", + "spring.cloud.bootstrap.enabled=true") + .run(context -> { + + assertThat(context).hasSingleBean(ReactiveVaultEndpointProvider.class); + + ReactiveVaultEndpointProvider endpointProvider = context + .getBean(ReactiveVaultEndpointProvider.class); + + endpointProvider.getVaultEndpoint().as(StepVerifier::create).assertNext(actual -> { + assertThat(actual.getPort()).isEqualTo(1234); + }).verifyComplete(); + }); + } + + @Test + public void shouldRegisterVaultEndpointAdapterBean() { + + this.contextRunner.withUserConfiguration(BridgedDiscoveryConfiguration.class) + .withPropertyValues("spring.cloud.vault.token=foo", "spring.cloud.vault.discovery.enabled=true", + "spring.cloud.bootstrap.enabled=true") + .run(context -> { + + assertThat(context).hasSingleBean(ReactiveVaultEndpointProvider.class); + + ReactiveVaultEndpointProvider endpointProvider = context + .getBean(ReactiveVaultEndpointProvider.class); + + endpointProvider.getVaultEndpoint().as(StepVerifier::create).assertNext(actual -> { + assertThat(actual.getPort()).isEqualTo(1234); + }).verifyComplete(); + }); + } + + @Test + public void shouldNotRegisterBeansIfDiscoveryDisabled() { + + this.contextRunner.withUserConfiguration(ReactiveDiscoveryConfiguration.class) + .withPropertyValues("spring.cloud.vault.token=foo", "spring.cloud.vault.discovery.enabled=false", + "spring.cloud.bootstrap.enabled=true") + .run(context -> { + + assertThat(context.getBeanNamesForType(ReactiveVaultEndpointProvider.class)).isEmpty(); + }); + } + + @Test + public void shouldNotRegisterBeansIfVaultDisabled() { + + this.contextRunner.withUserConfiguration(ReactiveDiscoveryConfiguration.class) + .withPropertyValues("spring.cloud.vault.token=foo", "spring.cloud.vault.enabled=false", + "spring.cloud.bootstrap.enabled=true") + .run(context -> { + + assertThat(context.getBeanNamesForType(ReactiveVaultEndpointProvider.class)).isEmpty(); + }); + + } + + @Configuration(proxyBeanMethods = false) + static class ReactiveDiscoveryConfiguration { + + @Bean + ReactiveDiscoveryClient reactiveDiscoveryClient() { + + ReactiveDiscoveryClient mock = Mockito.mock(ReactiveDiscoveryClient.class); + when(mock.getInstances(anyString())) + .thenReturn(Flux.just(new SimpleServiceInstance(URI.create("https://foo:1234")))); + + return mock; + } + + } + + @Configuration(proxyBeanMethods = false) + static class BridgedDiscoveryConfiguration { + + @Bean + VaultEndpointProvider vaultEndpointProvider() { + + VaultEndpointProvider mock = Mockito.mock(VaultEndpointProvider.class); + VaultEndpoint vaultEndpoint = VaultEndpoint.create("foo", 1234); + + when(mock.getVaultEndpoint()).thenReturn(vaultEndpoint); + + return mock; + } + + } + + static class SimpleServiceInstance implements ServiceInstance { + + private URI uri; + + private String host; + + private int port; + + private boolean secure; + + private Map metadata = new LinkedHashMap<>(); + + private String serviceId; + + SimpleServiceInstance(URI uri) { + this.setUri(uri); + } + + void setUri(URI uri) { + this.uri = uri; + this.host = this.uri.getHost(); + this.port = this.uri.getPort(); + String scheme = this.uri.getScheme(); + if ("https".equals(scheme)) { + this.secure = true; + } + + } + + public URI getUri() { + return this.uri; + } + + public String getHost() { + return this.host; + } + + public int getPort() { + return this.port; + } + + public boolean isSecure() { + return this.secure; + } + + public Map getMetadata() { + return this.metadata; + } + + public String getServiceId() { + return this.serviceId; + } + + public void setHost(String host) { + this.host = host; + } + + public void setPort(int port) { + this.port = port; + } + + public void setSecure(boolean secure) { + this.secure = secure; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + + public void setServiceId(String serviceId) { + this.serviceId = serviceId; + } + + } + +} diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultReactiveAutoConfigurationTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultReactiveAutoConfigurationTests.java index 0a85c95f..52e72e52 100644 --- a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultReactiveAutoConfigurationTests.java +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultReactiveAutoConfigurationTests.java @@ -21,6 +21,7 @@ import java.util.concurrent.atomic.AtomicLong; import org.junit.Test; import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.FilteredClassLoader; @@ -37,6 +38,8 @@ import org.springframework.vault.authentication.ReactiveSessionManager; import org.springframework.vault.authentication.SessionManager; import org.springframework.vault.authentication.SimpleSessionManager; import org.springframework.vault.authentication.VaultTokenSupplier; +import org.springframework.vault.client.ReactiveVaultEndpointProvider; +import org.springframework.vault.client.VaultEndpoint; import org.springframework.vault.client.WebClientFactory; import org.springframework.vault.core.ReactiveVaultOperations; import org.springframework.vault.core.ReactiveVaultTemplate; @@ -158,6 +161,29 @@ public class VaultReactiveAutoConfigurationTests { }); } + @Test + public void shouldConfigureEndpointProvider() { + + this.contextRunner + .withPropertyValues("spring.cloud.vault.kv.enabled=false", "spring.cloud.vault.token=foo", + "spring.cloud.vault.session.lifecycle.enabled=false") + .withUserConfiguration(ReactiveEndpointProviderConfiguration.class) + .withBean("vaultTokenSupplier", VaultTokenSupplier.class, () -> Mono::empty) + .withBean("taskSchedulerWrapper", VaultAutoConfiguration.TaskSchedulerWrapper.class, + () -> new VaultAutoConfiguration.TaskSchedulerWrapper(new ThreadPoolTaskScheduler())) + .run(context -> { + + WebClientFactory factory = context.getBean(WebClientFactory.class); + WebClient webClient = factory.create(); + + webClient.get().uri("foo").retrieve().bodyToMono(String.class).as(StepVerifier::create) + .verifyErrorMatches(throwable -> throwable.getMessage().contains("foobar-1")); + + webClient.get().uri("foo").retrieve().bodyToMono(String.class).as(StepVerifier::create) + .verifyErrorMatches(throwable -> throwable.getMessage().contains("foobar-2")); + }); + } + @Configuration(proxyBeanMethods = false) static class AuthenticationFactoryConfiguration { @@ -189,4 +215,23 @@ public class VaultReactiveAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class ReactiveEndpointProviderConfiguration { + + @Bean + ReactiveVaultEndpointProvider reactiveVaultEndpointProvider() { + + AtomicLong counter = new AtomicLong(); + + return () -> Mono.fromSupplier(() -> { + + VaultEndpoint vaultEndpoint = new VaultEndpoint(); + vaultEndpoint.setHost("foobar-" + counter.incrementAndGet()); + + return vaultEndpoint; + }); + } + + } + }