spring-cloud / spring-cloud-vault

Configuration Integration with HashiCorp Vault
http://cloud.spring.io/spring-cloud-vault/
Apache License 2.0
272 stars 151 forks source link

Usage of spring.cloud.vault.reactive.enabled property in VaultConfigDataLoader #619

Closed axbg closed 2 years ago

axbg commented 2 years ago

Hello,

I've encountered an issue in a work project I've been assigned to, and I wondered if something can be introduced to solve situations like mine, even though, a working, yet hacky "solution" already exists, and the situation itself is not ideal.

In short, I have a dependency that pulls in spring-webflux everywhere it's included, even when there's no intention to use a reactive context or WebFlux in a servlet-based application.

After updating to the latest version of Spring Cloud Vault, an error, claiming that no supported Reactive HTTP Client library is available started popping up.

This would be the relevant part of the stack trace.

[main] ERROR org.springframework.boot.SpringApplication - Application run failed
java.lang.IllegalStateException: No supported Reactive Http Client library available (Reactor Netty, Jetty)
    at org.springframework.vault.client.ClientHttpConnectorFactory.create(ClientHttpConnectorFactory.java:91)
    at org.springframework.cloud.vault.config.VaultReactiveConfiguration.createClientHttpConnector(VaultReactiveConfiguration.java:71)
    at org.springframework.cloud.vault.config.VaultConfigDataLoader$ReactiveInfrastructure.lambda$registerClientHttpConnectorWrapper$0(VaultConfigDataLoader.java:527)
    at org.springframework.cloud.vault.config.VaultConfigDataLoader.lambda$registerIfAbsent$12(VaultConfigDataLoader.java:339)
    at org.springframework.boot.DefaultBootstrapContext.getInstance(DefaultBootstrapContext.java:119)
    at org.springframework.boot.DefaultBootstrapContext.getOrElseThrow(DefaultBootstrapContext.java:111)
    at org.springframework.boot.DefaultBootstrapContext.get(DefaultBootstrapContext.java:88)
    at org.springframework.cloud.vault.config.VaultConfigDataLoader$ReactiveInfrastructure.lambda$registerWebClientFactory$3(VaultConfigDataLoader.java:540)
    at org.springframework.boot.DefaultBootstrapContext.getInstance(DefaultBootstrapContext.java:119)
    at org.springframework.boot.DefaultBootstrapContext.getOrElseThrow(DefaultBootstrapContext.java:111)
    at org.springframework.boot.DefaultBootstrapContext.get(DefaultBootstrapContext.java:88)
    at org.springframework.cloud.vault.config.VaultConfigDataLoader$ReactiveInfrastructure.lambda$registerTokenSupplier$6(VaultConfigDataLoader.java:548)
    at org.springframework.boot.DefaultBootstrapContext.getInstance(DefaultBootstrapContext.java:119)
    at org.springframework.boot.DefaultBootstrapContext.getOrElseThrow(DefaultBootstrapContext.java:111)
    at org.springframework.boot.DefaultBootstrapContext.get(DefaultBootstrapContext.java:88)
    at org.springframework.cloud.vault.config.VaultConfigDataLoader$ReactiveInfrastructure.lambda$registerReactiveSessionManager$8(VaultConfigDataLoader.java:566)
    at org.springframework.boot.DefaultBootstrapContext.getInstance(DefaultBootstrapContext.java:119)
    at org.springframework.boot.DefaultBootstrapContext.getOrElseThrow(DefaultBootstrapContext.java:111)
    at org.springframework.boot.DefaultBootstrapContext.get(DefaultBootstrapContext.java:88)
    at org.springframework.cloud.vault.config.VaultConfigDataLoader$ReactiveInfrastructure.lambda$registerSessionManager$9(VaultConfigDataLoader.java:574)
    at org.springframework.boot.DefaultBootstrapContext.getInstance(DefaultBootstrapContext.java:119)
    at org.springframework.boot.DefaultBootstrapContext.getOrElseThrow(DefaultBootstrapContext.java:111)
    at org.springframework.boot.DefaultBootstrapContext.get(DefaultBootstrapContext.java:88)
    at org.springframework.cloud.vault.config.VaultConfigDataLoader.lambda$registerImperativeInfrastructure$4(VaultConfigDataLoader.java:195)
    at org.springframework.boot.DefaultBootstrapContext.getInstance(DefaultBootstrapContext.java:119)
    at org.springframework.boot.DefaultBootstrapContext.getOrElseThrow(DefaultBootstrapContext.java:111)
    at org.springframework.boot.DefaultBootstrapContext.get(DefaultBootstrapContext.java:88)
    at org.springframework.cloud.vault.config.VaultConfigDataLoader.lambda$registerSecretLeaseContainer$10(VaultConfigDataLoader.java:251)
    at org.springframework.boot.DefaultBootstrapContext.getInstance(DefaultBootstrapContext.java:119)
    at org.springframework.boot.DefaultBootstrapContext.getOrElseThrow(DefaultBootstrapContext.java:111)
    at org.springframework.boot.DefaultBootstrapContext.get(DefaultBootstrapContext.java:88)
    at org.springframework.cloud.vault.config.VaultConfigDataLoader.loadConfigData(VaultConfigDataLoader.java:155)
    at org.springframework.cloud.vault.config.VaultConfigDataLoader.load(VaultConfigDataLoader.java:135)
    at org.springframework.cloud.vault.config.VaultConfigDataLoader.load(VaultConfigDataLoader.java:94)
    at org.springframework.boot.context.config.ConfigDataLoaders.load(ConfigDataLoaders.java:107)
    at org.springframework.boot.context.config.ConfigDataImporter.load(ConfigDataImporter.java:128)

After re-reading the documentation, I discovered the spring.cloud.vault.reactive.enabled property which, even when set to true, didn't prevent the reactive infrastructure from being set up.

I started debugging the application at startup and I saw that, indeed, in the VaultConfigDataLoader class, the enablement of the reactive infrastructure is determined only by the presence of the reactor.core.publisher.Flux and org.springframework.web.reactive.function.client.WebClient classes.

private final static boolean FLUX_AVAILABLE = ClassUtils.isPresent("reactor.core.publisher.Flux", VaultConfigDataLoader.class.getClassLoader());

private final static boolean WEBCLIENT_AVAILABLE = ClassUtils.isPresent("org.springframework.web.reactive.function.client.WebClient", VaultConfigDataLoader.class.getClassLoader());

private final static boolean REGISTER_REACTIVE_INFRASTRUCTURE = FLUX_AVAILABLE && WEBCLIENT_AVAILABLE;

//........

if (REGISTER_REACTIVE_INFRASTRUCTURE) {
    registerReactiveInfrastructure(bootstrap, vaultProperties);
}

In most cases, this behavior is good, as you'll need either Netty or Jetty HttpClients to use WebClient, but, in some specific cases where the presence of spring-webflux in the classpath is determined by other dependencies, it might become problematic.

A working solution, yet hacky solution is to use exclusions, but I raised this question because the existence of the spring.cloud.vault.reactive.enabled made me think that this could have been the desired behavior that was somehow overlooked.

I was curious if it would make sense to condition the reactive infrastructure registering by taking into account the value of the spring.cloud.vault.reactive.enabled property, together with the already existing check.

I think it would help developers to have a little bit more control over the library's behavior in certain conditions.

mp911de commented 2 years ago

Thanks for the report. Our previously used bootstrap configuration is already guarded with @ConditionalOnExpression("${spring.cloud.vault.reactive.enabled:true}") so the missing check in VaultConfigDataLoader is clearly a bug on our side.