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