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 a33022c4..0345ae68 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 @@ -40,12 +40,12 @@ import org.springframework.boot.context.config.ConfigDataLoaderContext; import org.springframework.boot.context.config.ConfigDataLocationNotFoundException; import org.springframework.boot.logging.DeferredLogFactory; import org.springframework.cloud.vault.config.VaultAutoConfiguration.TaskSchedulerWrapper; +import org.springframework.cloud.vault.config.VaultReactiveAutoConfiguration.ClientHttpConnectorWrapper; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.env.PropertySource; import org.springframework.http.client.ClientHttpRequestFactory; -import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.lang.Nullable; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.util.ClassUtils; @@ -170,7 +170,7 @@ public class VaultConfigDataLoader implements ConfigDataLoader this.configuration.createClientHttpConnector()); + void registerClientHttpConnectorWrapper() { + registerIfAbsent(this.bootstrap, "clientHttpConnectorWrapper", ClientHttpConnectorWrapper.class, + () -> new ClientHttpConnectorWrapper(this.configuration.createClientHttpConnector())); } public void registerWebClientBuilder() { // not a bean this.bootstrap.registerIfAbsent(WebClientBuilder.class, - ctx -> this.configuration.createWebClientBuilder(ctx.get(ClientHttpConnector.class), - this.endpointProvider, Collections.emptyList())); + ctx -> this.configuration.createWebClientBuilder( + ctx.get(ClientHttpConnectorWrapper.class).getConnector(), this.endpointProvider, + Collections.emptyList())); } void registerWebClientFactory() { registerIfAbsent(this.bootstrap, "vaultWebClientFactory", WebClientFactory.class, - ctx -> new DefaultWebClientFactory(ctx.get(ClientHttpConnector.class), + ctx -> new DefaultWebClientFactory(ctx.get(ClientHttpConnectorWrapper.class).getConnector(), connector -> this.configuration.createWebClientBuilder(connector, this.endpointProvider, Collections.emptyList()))); } 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 5f0e5edc..015ec025 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 @@ -24,10 +24,12 @@ import reactor.core.publisher.Flux; import reactor.netty.http.client.HttpClient; import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -35,6 +37,7 @@ 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.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.AnnotationAwareOrderComparator; @@ -79,6 +82,9 @@ import static org.springframework.cloud.vault.config.VaultAutoConfiguration.Task @AutoConfigureBefore(VaultAutoConfiguration.class) public class VaultReactiveAutoConfiguration implements InitializingBean { + @Nullable + private final ConfigurableApplicationContext applicationContext; + private final VaultProperties vaultProperties; private final VaultReactiveConfiguration configuration; @@ -91,20 +97,26 @@ public class VaultReactiveAutoConfiguration implements InitializingBean { private final List customizers; - @Nullable - private ClientHttpConnector clientHttpConnector; - /** - * Used for Vault communication. + * @deprecated since 3.0.2, use + * {@link #VaultReactiveAutoConfiguration(ConfigurableApplicationContext, VaultProperties, ObjectProvider, ObjectProvider, ObjectProvider)} + * instead. */ - @Nullable - private WebClientBuilder webClientBuilder; - + @Deprecated public VaultReactiveAutoConfiguration(VaultProperties vaultProperties, ObjectProvider reactiveEndpointProvider, ObjectProvider endpointProvider, ObjectProvider> webClientCustomizers) { + this(null, vaultProperties, reactiveEndpointProvider, endpointProvider, webClientCustomizers); + } + @Autowired + public VaultReactiveAutoConfiguration(@Nullable ConfigurableApplicationContext applicationContext, + VaultProperties vaultProperties, ObjectProvider reactiveEndpointProvider, + ObjectProvider endpointProvider, + ObjectProvider> webClientCustomizers) { + + this.applicationContext = applicationContext; this.vaultProperties = vaultProperties; this.configuration = new VaultReactiveConfiguration(vaultProperties); @@ -122,14 +134,6 @@ public class VaultReactiveAutoConfiguration implements InitializingBean { AnnotationAwareOrderComparator.sort(this.customizers); } - @Override - public void afterPropertiesSet() { - - this.clientHttpConnector = createConnector(this.vaultProperties); - - this.webClientBuilder = webClientBuilder(this.clientHttpConnector); - } - protected WebClientBuilder webClientBuilder(ClientHttpConnector connector) { if (this.reactiveEndpointProvider != null) { @@ -145,6 +149,26 @@ public class VaultReactiveAutoConfiguration implements InitializingBean { "Cannot create WebClientBuilder as neither ReactiveEndpointProvider nor EndpointProvider configured"); } + /** + * Creates a {@link ClientHttpConnectorWrapper} containing a + * {@link ClientHttpConnector}. {@link ClientHttpConnector} is not exposed as root + * bean because {@link ClientHttpConnector} is configured with {@link ClientOptions} + * and {@link SslConfiguration} which are not necessarily applicable for the whole + * application. + * @return the {@link ClientHttpConnectorWrapper} to wrap a + * {@link ClientHttpConnector} instance. + * @since 3.0.2 + */ + @Bean + @ConditionalOnMissingBean + public ClientHttpConnectorWrapper clientHttpConnectorWrapper() { + return new ClientHttpConnectorWrapper(createConnector(this.vaultProperties)); + } + + @Override + public void afterPropertiesSet() { + } + /** * Creates a {@link ClientHttpConnector} configured with {@link ClientOptions} and * {@link SslConfiguration} which are not necessarily applicable for the whole @@ -165,9 +189,18 @@ public class VaultReactiveAutoConfiguration implements InitializingBean { @ConditionalOnMissingBean public WebClientFactory vaultWebClientFactory() { - Assert.state(this.clientHttpConnector != null, "ClientHttpConnector must not be null"); + ClientHttpConnector connector = getClientHttpConnector(); - return new DefaultWebClientFactory(this.clientHttpConnector, this::webClientBuilder); + return new DefaultWebClientFactory(connector, this::webClientBuilder); + } + + protected ClientHttpConnector getClientHttpConnector() { + + if (this.applicationContext != null) { + return this.applicationContext.getBean(ClientHttpConnectorWrapper.class).getConnector(); + } + + return createConnector(this.vaultProperties); } /** @@ -180,13 +213,13 @@ public class VaultReactiveAutoConfiguration implements InitializingBean { @ConditionalOnMissingBean(ReactiveVaultOperations.class) public ReactiveVaultTemplate reactiveVaultTemplate(ObjectProvider sessionManager) { - Assert.state(this.webClientBuilder != null, "WebClientBuilder must not be null"); + WebClientBuilder webClientBuilder = webClientBuilder(getClientHttpConnector()); if (this.vaultProperties.getAuthentication() == VaultProperties.AuthenticationMethod.NONE) { - return new ReactiveVaultTemplate(this.webClientBuilder); + return new ReactiveVaultTemplate(webClientBuilder); } - return new ReactiveVaultTemplate(this.webClientBuilder, sessionManager.getObject()); + return new ReactiveVaultTemplate(webClientBuilder, sessionManager.getObject()); } /** @@ -241,4 +274,38 @@ public class VaultReactiveAutoConfiguration implements InitializingBean { () -> beanFactory.getBeanProvider(ClientAuthentication.class, false).getIfAvailable()); } + /** + * Wrapper for {@link ClientHttpConnector} to not expose the bean globally. + * + * @since 3.0.2 + */ + public static class ClientHttpConnectorWrapper implements InitializingBean, DisposableBean { + + private final ClientHttpConnector connector; + + public ClientHttpConnectorWrapper(ClientHttpConnector connector) { + this.connector = connector; + } + + @Override + public void destroy() throws Exception { + if (this.connector instanceof DisposableBean) { + ((DisposableBean) this.connector).destroy(); + } + } + + @Override + public void afterPropertiesSet() throws Exception { + + if (this.connector instanceof InitializingBean) { + ((InitializingBean) this.connector).afterPropertiesSet(); + } + } + + public ClientHttpConnector getConnector() { + return this.connector; + } + + } + } 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 52e72e52..db523eb5 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 @@ -28,6 +28,7 @@ import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.vault.authentication.AuthenticationSteps; @@ -45,8 +46,12 @@ import org.springframework.vault.core.ReactiveVaultOperations; import org.springframework.vault.core.ReactiveVaultTemplate; import org.springframework.vault.support.VaultToken; import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientRequestException; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Tests for {@link VaultReactiveAutoConfiguration}. @@ -184,6 +189,24 @@ public class VaultReactiveAutoConfigurationTests { }); } + @Test + public void shouldConsiderCustomConnector() { + + this.contextRunner + .withPropertyValues("spring.cloud.vault.kv.enabled=false", "spring.cloud.vault.token=foo", + "spring.cloud.vault.session.lifecycle.enabled=false") + .withUserConfiguration(CustomConnector.class) + .withBean("vaultTokenSupplier", VaultTokenSupplier.class, + () -> () -> Mono.just(VaultToken.of("foo".toCharArray()))) + .withBean("taskSchedulerWrapper", VaultAutoConfiguration.TaskSchedulerWrapper.class, + () -> new VaultAutoConfiguration.TaskSchedulerWrapper(new ThreadPoolTaskScheduler())) + .run(context -> { + + ReactiveVaultOperations operations = context.getBean(ReactiveVaultOperations.class); + operations.delete("foo").as(StepVerifier::create).verifyError(WebClientRequestException.class); + }); + } + @Configuration(proxyBeanMethods = false) static class AuthenticationFactoryConfiguration { @@ -234,4 +257,17 @@ public class VaultReactiveAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class CustomConnector { + + @Bean + VaultReactiveAutoConfiguration.ClientHttpConnectorWrapper myWrapper() { + ClientHttpConnector mock = mock(ClientHttpConnector.class); + when(mock.connect(any(), any(), any())).thenReturn(Mono.error(new UnsupportedOperationException())); + + return new VaultReactiveAutoConfiguration.ClientHttpConnectorWrapper(mock); + } + + } + }