From 4a471531b1fbe4965b162d38c8e16eafe9ec16bc Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 13 Jul 2017 22:55:38 +0200 Subject: [PATCH] Allow Vault server lookup via DiscoveryClient. We now support Vault server lookup using service registries via DiscoveryClient. Vault server is looked up during the bootstrap phase and configured by serviceId rather than host/port when enabling service discovery. Closes gh-132. --- .../src/main/asciidoc/spring-cloud-vault.adoc | 27 ++++++ spring-cloud-vault-config-consul/pom.xml | 39 +++++++- .../DiscoveryBootstrapConfigurationTests.java | 85 ++++++++++++++++ spring-cloud-vault-config/pom.xml | 5 + ...veryClientVaultBootstrapConfiguration.java | 97 +++++++++++++++++++ .../config/VaultBootstrapConfiguration.java | 24 +++-- .../cloud/vault/config/VaultProperties.java | 22 +++++ .../config/VaultServerInstanceProvider.java | 55 +++++++++++ .../main/resources/META-INF/spring.factories | 1 + 9 files changed, 348 insertions(+), 7 deletions(-) create mode 100644 spring-cloud-vault-config-consul/src/test/java/org/springframework/cloud/vault/config/consul/DiscoveryBootstrapConfigurationTests.java create mode 100644 spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/DiscoveryClientVaultBootstrapConfiguration.java create mode 100644 spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultServerInstanceProvider.java diff --git a/docs/src/main/asciidoc/spring-cloud-vault.adoc b/docs/src/main/asciidoc/spring-cloud-vault.adoc index adc2f519..0fd5173e 100644 --- a/docs/src/main/asciidoc/spring-cloud-vault.adoc +++ b/docs/src/main/asciidoc/spring-cloud-vault.adoc @@ -789,6 +789,33 @@ NOTE: All customization is required to happen in the bootstrap context. Add your classes to `META-INF/spring.factories` at `org.springframework.cloud.bootstrap.BootstrapConfiguration` in your application. +== Service Registry Configuration + +You can use a `DiscoveryClient` (such as from Spring Cloud Consul) to locate +a Vault server by setting spring.cloud.vault.discovery.enabled=true (default `false`). +The net result of that is that your apps need a bootstrap.yml (or an environment variable) +with the appropriate discovery configuration. +The benefit is that the Vault can change its co-ordinates, as long as the discovery service +is a fixed point. The default service id is `vault` but you can change that on the client with +`spring.cloud.vault.discovery.serviceId`. + +The discovery client implementations all support some kind of metadata map +(e.g. for Eureka we have eureka.instance.metadataMap). Some additional properties of the service +may need to be configured in its service registration metadata so that clients can connect +correctly. Service registries that do not provide details about transport layer security +need to provide a `scheme` metadata entry to be set either to `https` or `http`. +If no scheme is configured and the service is not exposed as secure service, then +configuration defaults to `spring.cloud.vault.scheme` which is `https` when it's not set. + +==== +[source,yaml] +---- +spring.cloud.vault.discovery: + enabled: true + service-id: my-vault-service +---- +==== + [[vault.config.fail-fast]] == Vault Client Fail Fast diff --git a/spring-cloud-vault-config-consul/pom.xml b/spring-cloud-vault-config-consul/pom.xml index 36350747..c74edd67 100644 --- a/spring-cloud-vault-config-consul/pom.xml +++ b/spring-cloud-vault-config-consul/pom.xml @@ -1,5 +1,7 @@ - + 4.0.0 @@ -13,6 +15,10 @@ Spring Cloud Vault Config Consul support Spring Cloud Vault Config Consul support + + 2.0.0.BUILD-SNAPSHOT + + org.springframework.cloud @@ -25,6 +31,37 @@ test-jar + + org.springframework.cloud + spring-cloud-commons + + + + org.springframework.cloud + spring-cloud-starter-consul-discovery + ${spring-cloud-consul.version} + test + + + org.springframework.cloud + spring-cloud-starter-ribbon + + + + + + org.springframework.cloud + spring-cloud-starter-consul-config + ${spring-cloud-consul.version} + test + + + org.springframework.cloud + spring-cloud-starter-ribbon + + + + com.squareup.okhttp3 okhttp diff --git a/spring-cloud-vault-config-consul/src/test/java/org/springframework/cloud/vault/config/consul/DiscoveryBootstrapConfigurationTests.java b/spring-cloud-vault-config-consul/src/test/java/org/springframework/cloud/vault/config/consul/DiscoveryBootstrapConfigurationTests.java new file mode 100644 index 00000000..ba6d8be4 --- /dev/null +++ b/spring-cloud-vault-config-consul/src/test/java/org/springframework/cloud/vault/config/consul/DiscoveryBootstrapConfigurationTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2017 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 + * + * http://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.consul; + +import java.net.InetSocketAddress; +import java.util.List; + +import com.ecwid.consul.v1.ConsulClient; +import com.ecwid.consul.v1.QueryParams; +import com.ecwid.consul.v1.Response; +import com.ecwid.consul.v1.agent.model.NewService; +import com.ecwid.consul.v1.catalog.model.CatalogService; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.vault.util.CanConnect; +import org.springframework.cloud.vault.util.IntegrationTestSupport; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.vault.core.VaultOperations; +import org.springframework.vault.support.VaultHealth; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assume.assumeTrue; + +/** + * Tests for {@link DiscoveryBootstrapConfigurationTests}. + * + * @author Mark Paluch + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(properties = { "spring.cloud.vault.discovery.enabled=true" }) +public class DiscoveryBootstrapConfigurationTests extends IntegrationTestSupport { + + private final static String CONSUL_HOST = "localhost"; + private final static int CONSUL_PORT = 8500; + + @Autowired + VaultOperations vaultOperations; + + @BeforeClass + public static void beforeClass() { + + assumeTrue(CanConnect.to(new InetSocketAddress(CONSUL_HOST, CONSUL_PORT))); + + ConsulClient client = new ConsulClient(); + + Response> response = client.getCatalogService("vault", + QueryParams.DEFAULT); + + if (response.getValue().isEmpty()) { + + NewService service = new NewService(); + service.setAddress("localhost"); + service.setPort(8200); + service.setId("vault"); + service.setName("vault"); + + client.agentServiceRegister(service); + } + } + + @Test + public void shouldDiscoverThroughConsul() { + + VaultHealth health = vaultOperations.opsForSys().health(); + + assertThat(health).isNotNull(); + } +} diff --git a/spring-cloud-vault-config/pom.xml b/spring-cloud-vault-config/pom.xml index 2a773dec..e4a571bb 100644 --- a/spring-cloud-vault-config/pom.xml +++ b/spring-cloud-vault-config/pom.xml @@ -31,6 +31,11 @@ true + + org.springframework.cloud + spring-cloud-commons + + org.springframework.cloud spring-cloud-context 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 new file mode 100644 index 00000000..3f37a7b0 --- /dev/null +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/DiscoveryClientVaultBootstrapConfiguration.java @@ -0,0 +1,97 @@ +/* + * Copyright 2017 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 + * + * http://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 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.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +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.util.StringUtils; +import org.springframework.vault.client.VaultEndpoint; +import org.springframework.vault.client.VaultEndpointProvider; + +/** + * {@link org.springframework.cloud.bootstrap.BootstrapConfiguration} providing a + * {@link VaultEndpointProvider} using {@link DiscoveryClient}. + * + * @author Mark Paluch + * @since 1.1 + */ +@Configuration +@ConditionalOnProperty(value = "spring.cloud.vault.discovery.enabled") +@EnableConfigurationProperties(VaultProperties.class) +@Order(Ordered.LOWEST_PRECEDENCE - 2) +@EnableDiscoveryClient +@Import(UtilAutoConfiguration.class) +public class DiscoveryClientVaultBootstrapConfiguration { + + private final VaultProperties vaultProperties; + + private final VaultServerInstanceProvider instanceProvider; + + public DiscoveryClientVaultBootstrapConfiguration(VaultProperties vaultProperties, + DiscoveryClient discoveryClient) { + + this.vaultProperties = vaultProperties; + this.instanceProvider = new VaultServerInstanceProvider(discoveryClient); + } + + @Bean + @ConditionalOnMissingBean + public VaultEndpointProvider vaultEndpointProvider() { + + final String serviceId = this.vaultProperties.getDiscovery().getServiceId(); + + final String fallbackScheme; + + if (StringUtils.hasText(this.vaultProperties.getUri())) { + fallbackScheme = URI.create(this.vaultProperties.getUri()).getScheme(); + } + else { + fallbackScheme = this.vaultProperties.getScheme(); + } + + ServiceInstance server = instanceProvider.getVaultServerInstance(serviceId); + + final 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 new VaultEndpointProvider() { + + @Override + public VaultEndpoint getVaultEndpoint() { + return vaultEndpoint; + } + }; + } +} 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 c14604ff..1f6eb841 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 @@ -21,6 +21,7 @@ import java.time.Duration; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -40,8 +41,10 @@ import org.springframework.vault.authentication.ClientAuthentication; import org.springframework.vault.authentication.LifecycleAwareSessionManager; import org.springframework.vault.authentication.SessionManager; import org.springframework.vault.authentication.SimpleSessionManager; +import org.springframework.vault.client.SimpleVaultEndpointProvider; import org.springframework.vault.client.VaultClients; import org.springframework.vault.client.VaultEndpoint; +import org.springframework.vault.client.VaultEndpointProvider; import org.springframework.vault.config.ClientHttpRequestFactoryFactory; import org.springframework.vault.config.AbstractVaultConfiguration.ClientFactoryWrapper; import org.springframework.vault.core.VaultTemplate; @@ -66,19 +69,27 @@ public class VaultBootstrapConfiguration implements InitializingBean { private final VaultProperties vaultProperties; - private final VaultEndpoint vaultEndpoint; + private final VaultEndpointProvider endpointProvider; private RestOperations restOperations; public VaultBootstrapConfiguration(ConfigurableApplicationContext applicationContext, - VaultProperties vaultProperties) { + VaultProperties vaultProperties, + ObjectProvider endpointProvider) { this.applicationContext = applicationContext; this.vaultProperties = vaultProperties; - this.vaultEndpoint = getVaultEndpoint(vaultProperties); + + VaultEndpointProvider provider = endpointProvider.getIfAvailable(); + + if (provider == null) { + provider = SimpleVaultEndpointProvider.of(getVaultEndpoint(vaultProperties)); + } + + this.endpointProvider = provider; } - private VaultEndpoint getVaultEndpoint(VaultProperties vaultProperties) { + private static VaultEndpoint getVaultEndpoint(VaultProperties vaultProperties) { if (StringUtils.hasText(vaultProperties.getUri())) { return VaultEndpoint.from(URI.create(vaultProperties.getUri())); @@ -92,6 +103,7 @@ public class VaultBootstrapConfiguration implements InitializingBean { return vaultEndpoint; } + @Override @SuppressWarnings("unchecked") public void afterPropertiesSet() { @@ -99,7 +111,7 @@ public class VaultBootstrapConfiguration implements InitializingBean { ClientHttpRequestFactory clientHttpRequestFactory = clientHttpRequestFactoryWrapper() .getClientHttpRequestFactory(); - this.restOperations = VaultClients.createRestTemplate(vaultEndpoint, + this.restOperations = VaultClients.createRestTemplate(endpointProvider, clientHttpRequestFactory); } @@ -168,7 +180,7 @@ public class VaultBootstrapConfiguration implements InitializingBean { @Bean @ConditionalOnMissingBean public VaultTemplate vaultTemplate(SessionManager sessionManager) { - return new VaultTemplate(vaultEndpoint, clientHttpRequestFactoryWrapper() + return new VaultTemplate(endpointProvider, clientHttpRequestFactoryWrapper() .getClientHttpRequestFactory(), sessionManager); } diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java index 4c3f0bc5..374e2979 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java @@ -62,6 +62,11 @@ public class VaultProperties implements EnvironmentAware { */ private String uri; + /** + * Discovery properties. + */ + private Discovery discovery = new Discovery(); + /** * Connection timeout; */ @@ -109,6 +114,23 @@ public class VaultProperties implements EnvironmentAware { } } + @Data + public static class Discovery { + + public static final String DEFAULT_VAULT = "vault"; + + /** + * Flag to indicate that Vault server discovery is enabled (vault server URL will + * be looked up via discovery). + */ + private boolean enabled; + + /** + * Service id to locate Vault. + */ + private String serviceId = DEFAULT_VAULT; + } + @Data @Validated public static class AppIdProperties { diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultServerInstanceProvider.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultServerInstanceProvider.java new file mode 100644 index 00000000..ee6b5641 --- /dev/null +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultServerInstanceProvider.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017 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 + * + * http://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.util.List; + +import lombok.RequiredArgsConstructor; +import lombok.extern.apachecommons.CommonsLog; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; + +/** + * Provider for {@link ServiceInstance} to look up the Vault service. + * + * @author Mark Paluch + * @since 1.1 + */ +@CommonsLog +@RequiredArgsConstructor +class VaultServerInstanceProvider { + + private final DiscoveryClient client; + + ServiceInstance getVaultServerInstance(String serviceId) { + + log.debug("Locating Vault server (" + serviceId + ") via discovery"); + + List instances = this.client.getInstances(serviceId); + + if (instances.isEmpty()) { + throw new IllegalStateException("No instances found of Vault server (" + + serviceId + ")"); + } + + ServiceInstance instance = instances.get(0); + + log.debug("Located Vault server (" + serviceId + ") via discovery: " + instance); + + return instance; + } +} 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 1ce5dd5e..624d5011 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 @@ -3,6 +3,7 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.cloud.vault.config.VaultHealthIndicatorConfiguration # Bootstrap Configuration org.springframework.cloud.bootstrap.BootstrapConfiguration=\ +org.springframework.cloud.vault.config.DiscoveryClientVaultBootstrapConfiguration,\ org.springframework.cloud.vault.config.VaultBootstrapConfiguration,\ org.springframework.cloud.vault.config.ReactiveVaultBootstrapConfiguration,\ org.springframework.cloud.vault.config.VaultBootstrapPropertySourceConfiguration