diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/GenericSecretBackendMetadata.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/GenericSecretBackendMetadata.java index 1f27153b..27170775 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/GenericSecretBackendMetadata.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/GenericSecretBackendMetadata.java @@ -15,10 +15,16 @@ */ package org.springframework.cloud.vault.config; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; +import org.springframework.core.env.Environment; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import org.springframework.vault.core.util.PropertyTransformer; import org.springframework.vault.core.util.PropertyTransformers; @@ -77,4 +83,58 @@ class GenericSecretBackendMetadata implements SecretBackendMetadata { return variables; } + + /** + * Build a list of context paths from application name and the active profile names. + * Application name and profiles support multiple (comma-separated) values. + * + * @param genericBackendProperties + * @param environment + * @return + */ + public static List buildContexts( + VaultGenericBackendProperties genericBackendProperties, + Environment environment) { + + String appName = genericBackendProperties.getApplicationName(); + List profiles = Arrays.asList(environment.getActiveProfiles()); + List contexts = new ArrayList<>(); + + String defaultContext = genericBackendProperties.getDefaultContext(); + addContext(contexts, defaultContext, profiles, genericBackendProperties); + + for (String applicationName : StringUtils.commaDelimitedListToSet(appName)) { + addContext(contexts, applicationName, profiles, genericBackendProperties); + } + + Collections.reverse(contexts); + return contexts; + } + + private static void addContext(List contexts, String applicationName, + List profiles, + VaultGenericBackendProperties genericBackendProperties) { + + if (!StringUtils.hasText(applicationName)) { + return; + } + + if (!contexts.contains(applicationName)) { + contexts.add(applicationName); + } + + for (String profile : profiles) { + + if (!StringUtils.hasText(profile)) { + continue; + } + + String contextName = applicationName + + genericBackendProperties.getProfileSeparator() + profile.trim(); + + if (!contexts.contains(contextName)) { + contexts.add(contextName); + } + } + } } diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/Lease.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/Lease.java deleted file mode 100644 index a94b5391..00000000 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/Lease.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2016 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 org.springframework.util.Assert; - -import lombok.EqualsAndHashCode; -import lombok.ToString; - -/** - * - * @author Mark Paluch - */ -@EqualsAndHashCode -@ToString -class Lease { - - private final String leaseId; - - private final long leaseDuration; - - private final boolean renewable; - - private Lease(String leaseId, long leaseDuration, boolean renewable) { - - Assert.hasText(leaseId, "LeaseId must not be empty"); - - this.leaseId = leaseId; - this.leaseDuration = leaseDuration; - this.renewable = renewable; - } - - /** - * Creates a new {@link Lease}. - * - * @param leaseId must not be empty or {@literal null}. - * @param leaseDuration the lease duration in seconds - * @param renewable {@literal true} if this lease is renewable. - * @return the created {@link Lease} - */ - public static Lease of(String leaseId, long leaseDuration, boolean renewable) { - return new Lease(leaseId, leaseDuration, renewable); - } - - /** - * - * @return the lease Id - */ - public String getLeaseId() { - return leaseId; - } - - /** - * - * @return - */ - public long getLeaseDuration() { - return leaseDuration; - } - - /** - * - * @return {@literal true} if the lease is renewable. - */ - public boolean isRenewable() { - return renewable; - } -} diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingVaultPropertySource.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingVaultPropertySource.java deleted file mode 100644 index 034e7a80..00000000 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingVaultPropertySource.java +++ /dev/null @@ -1,400 +0,0 @@ -/* - * Copyright 2016-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.Date; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - -import lombok.extern.apachecommons.CommonsLog; - -import org.springframework.beans.factory.DisposableBean; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; -import org.springframework.scheduling.TaskScheduler; -import org.springframework.scheduling.Trigger; -import org.springframework.scheduling.TriggerContext; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; -import org.springframework.vault.VaultException; -import org.springframework.vault.client.VaultResponses; -import org.springframework.vault.core.RestOperationsCallback; -import org.springframework.web.client.HttpStatusCodeException; -import org.springframework.web.client.RestOperations; - -/** - * A {@link VaultPropertySource} that renews a {@link Lease} associated with - * {@link Secrets}. - * - *

- * {@link Lease} is scheduled right before its expiry. Expiry threshold can be set by - * calling {@link #setExpiryThresholdSeconds(int)}. Leases that reached their maximum - * lifetime are not re-read from Vault. - * - * @author Mark Paluch - */ -@CommonsLog -class LeasingVaultPropertySource extends VaultPropertySource implements DisposableBean { - - private final LeaseRenewalScheduler leaseRenewal; - - private int minRenewalSeconds = 10; - - private int expiryThresholdSeconds = 60; - - private volatile Lease lease; - - /** - * Creates a new {@link VaultPropertySource}. - * - * @param operations must not be {@literal null}. - * @param failFast fail if properties could not be read because of access errors. - * @param secretBackendMetadata must not be {@literal null}. - * @param taskScheduler must not be {@literal null}. - */ - public LeasingVaultPropertySource(VaultConfigOperations operations, boolean failFast, - SecretBackendMetadata secretBackendMetadata, TaskScheduler taskScheduler) { - - super(operations, failFast, secretBackendMetadata); - - Assert.notNull(taskScheduler, "TaskScheduler must not be null"); - - leaseRenewal = new LeaseRenewalScheduler(taskScheduler); - } - - /** - * Set the expiry threshold. {@link Lease} is renewed the given seconds before it - * expires. - * - * @param expiryThresholdSeconds number of seconds before {@link Lease} expiry. - */ - public void setExpiryThresholdSeconds(int expiryThresholdSeconds) { - this.expiryThresholdSeconds = expiryThresholdSeconds; - } - - /** - * Sets the amount of seconds that is at least required before renewing a lease. - * {@code minRenewalSeconds} prevents renewals to happen too often. - * - * @param minRenewalSeconds number of seconds that is at least required before - * renewing a {@link Lease}. - */ - public void setMinRenewalSeconds(int minRenewalSeconds) { - this.minRenewalSeconds = minRenewalSeconds; - } - - @Override - public void init() { - - super.init(); - - Secrets secrets = getSecrets(); - - this.lease = getLease(secrets); - - potentiallyScheduleLeaseRenewal(this.lease); - } - - /** - * Shutdown this {@link LeasingVaultPropertySource} - */ - public void destroy() { - - if (this.lease != null) { - try { - leaseRenewal.disableScheduleRenewal(); - doRevokeLease(this.lease); - } - finally { - this.lease = null; - } - } - } - - private Lease getLease(Secrets secrets) { - - if (secrets == null || !StringUtils.hasText(secrets.getLeaseId())) { - return null; - } - - return Lease.of(secrets.getLeaseId(), secrets.getLeaseDuration(), - secrets.isRenewable()); - } - - private void potentiallyScheduleLeaseRenewal(Lease lease) { - - if (leaseRenewal.isLeaseRenewable(lease)) { - - if (log.isDebugEnabled()) { - log.debug(String.format("Lease %s qualified for renewal", - lease.getLeaseId())); - } - - leaseRenewal.scheduleRenewal(new RenewLease() { - @Override - public Lease renewLease(Lease lease) { - - Lease newLease = doRenewLease(lease); - LeasingVaultPropertySource.this.lease = newLease; - potentiallyScheduleLeaseRenewal(newLease); - - return newLease; - } - }, lease, minRenewalSeconds, expiryThresholdSeconds); - } - } - - /** - * Renews a {@link Lease}. - * - * @param lease the lease - * @return the new lease. - */ - private Lease doRenewLease(final Lease lease) { - - ResponseEntity> entity = null; - try { - entity = getSource().getVaultOperations().doWithSession( - new RestOperationsCallback>>() { - - @Override - @SuppressWarnings("unchecked") - public ResponseEntity> doWithRestOperations( - RestOperations restOperations) { - return (ResponseEntity) restOperations.exchange( - "/sys/renew/{leaseId}", HttpMethod.PUT, null, - Map.class, lease.getLeaseId()); - } - }); - - Map body = entity.getBody(); - String leaseId = (String) body.get("lease_id"); - Number leaseDuration = (Number) body.get("lease_duration"); - boolean renewable = (Boolean) body.get("renewable"); - - if (!StringUtils.hasText(leaseId)) { - return null; - } - - return Lease.of(leaseId, - leaseDuration != null ? leaseDuration.longValue() : 0, renewable); - } - catch (HttpStatusCodeException e) { - throw new VaultException(String.format("Cannot renew lease: %s", - VaultResponses.getError(e.getResponseBodyAsString()))); - } - - } - - /** - * Revokes the {@link Lease}. - * - * @param lease the lease. - */ - private void doRevokeLease(final Lease lease) { - - try { - getSource().getVaultOperations().doWithSession( - new RestOperationsCallback>>() { - - @Override - @SuppressWarnings("unchecked") - public ResponseEntity> doWithRestOperations( - RestOperations restOperations) { - return (ResponseEntity) restOperations.exchange( - "/sys/revoke/{leaseId}", HttpMethod.PUT, null, - Map.class, lease.getLeaseId()); - } - }); - } - catch (HttpStatusCodeException e) { - throw new VaultException(String.format("Cannot revoke lease: %s", - VaultResponses.getError(e.getResponseBodyAsString()))); - } - - } - - /** - * Abstracts scheduled lease renewal. A {@link LeaseRenewalScheduler} can be accessed - * concurrently to schedule lease renewal. Each renewal run checks if the previously - * attached {@link Lease} is still relevant to update. If any other process scheduled - * a newer {@link Lease} for renewal, the previously registered renewal task will skip - * renewal. - */ - @CommonsLog - private static class LeaseRenewalScheduler { - - private final TaskScheduler taskScheduler; - - private final AtomicReference currentLease = new AtomicReference<>(); - - private final Map> schedules = new ConcurrentHashMap<>(); - - /** - * - * @param taskScheduler must not be {@literal null}. - */ - LeaseRenewalScheduler(TaskScheduler taskScheduler) { - this.taskScheduler = taskScheduler; - } - - /** - * Schedule {@link Lease} renewal. Previously registered renewal tasks are - * canceled to prevent renewal of stale {@link Lease}s. - * @param renewLease strategy to renew a {@link Lease}. - * @param lease the current {@link Lease}. - * @param minRenewalSeconds minimum number of seconds before renewing a - * {@link Lease}. This is to prevent too many renewals in a very short timeframe. - * @param expiryThresholdSeconds number of seconds to renew before {@link Lease}. - * expires. - */ - void scheduleRenewal(final RenewLease renewLease, final Lease lease, - final int minRenewalSeconds, final int expiryThresholdSeconds) { - - if (log.isDebugEnabled()) { - log.debug(String.format( - "Scheduling renewal for lease %s, lease duration %d", - lease.getLeaseId(), lease.getLeaseDuration())); - } - - Lease currentLease = this.currentLease.get(); - this.currentLease.set(lease); - - if (currentLease != null) { - cancelSchedule(currentLease); - } - - ScheduledFuture scheduledFuture = taskScheduler.schedule(new Runnable() { - - @Override - public void run() { - - try { - - schedules.remove(lease); - - if (LeaseRenewalScheduler.this.currentLease.get() != lease) { - log.debug("Current lease has changed. Skipping renewal"); - return; - } - - if (log.isDebugEnabled()) { - log.debug(String.format("Renewing lease %s", - lease.getLeaseId())); - } - - LeaseRenewalScheduler.this.currentLease.compareAndSet(lease, - renewLease.renewLease(lease)); - } - catch (Exception e) { - log.error(String.format("Cannot renew lease %s", - lease.getLeaseId()), e); - } - } - }, new OneShotTrigger( - getRenewalSeconds(lease, minRenewalSeconds, expiryThresholdSeconds))); - - schedules.put(lease, scheduledFuture); - } - - private void cancelSchedule(Lease lease) { - - ScheduledFuture scheduledFuture = schedules.get(lease); - if (scheduledFuture != null) { - - if (log.isDebugEnabled()) { - log.debug(String.format( - "Canceling previously registered schedule for lease %s", - lease.getLeaseId())); - } - - scheduledFuture.cancel(false); - } - } - - /** - * Disables schedule for already scheduled renewals. - */ - public void disableScheduleRenewal() { - - currentLease.set(null); - Set leases = new HashSet<>(schedules.keySet()); - - for (Lease lease : leases) { - cancelSchedule(lease); - schedules.remove(lease); - } - } - - private long getRenewalSeconds(Lease lease, int minRenewalSeconds, - int expiryThresholdSeconds) { - return Math.max(minRenewalSeconds, - lease.getLeaseDuration() - expiryThresholdSeconds); - } - - private boolean isLeaseRenewable(Lease lease) { - return lease != null && lease.isRenewable(); - } - - } - - /** - * This one-shot trigger creates only one execution time to trigger an execution only - * once. - */ - private static class OneShotTrigger implements Trigger { - - private final AtomicBoolean fired = new AtomicBoolean(); - - private final long seconds; - - OneShotTrigger(long seconds) { - this.seconds = seconds; - } - - @Override - public Date nextExecutionTime(TriggerContext triggerContext) { - - if (fired.compareAndSet(false, true)) { - return new Date( - System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(seconds)); - } - - return null; - } - } - - /** - * Strategy interface to renew a {@link Lease}. - */ - private interface RenewLease { - - /** - * Renew a lease. - * - * @param lease must not be {@literal null}. - * @return the new lease - * @throws VaultException if lease renewal runs into problems - */ - Lease renewLease(Lease lease) throws VaultException; - } -} diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceLocator.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceLocator.java index d0e717ba..cbe0492f 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceLocator.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceLocator.java @@ -15,92 +15,135 @@ */ package org.springframework.cloud.vault.config; +import java.net.URI; import java.util.Collection; -import java.util.HashSet; -import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; import lombok.extern.apachecommons.CommonsLog; -import org.springframework.beans.factory.DisposableBean; +import org.springframework.core.PriorityOrdered; import org.springframework.core.env.PropertySource; -import org.springframework.scheduling.TaskScheduler; import org.springframework.util.Assert; +import org.springframework.vault.VaultException; +import org.springframework.vault.core.env.LeaseAwareVaultPropertySource; +import org.springframework.vault.core.lease.SecretLeaseContainer; +import org.springframework.vault.core.lease.domain.RequestedSecret; +import org.springframework.vault.core.lease.event.LeaseErrorListener; +import org.springframework.vault.core.lease.event.SecretLeaseEvent; +import org.springframework.web.util.DefaultUriTemplateHandler; +import org.springframework.web.util.UriTemplateHandler; /** * Extension to {@link LeasingVaultPropertySourceLocator} that creates - * {@link LeasingVaultPropertySource}s. + * {@link LeaseAwareVaultPropertySource}s. * * @author Mark Paluch - * @see LeasingVaultPropertySource + * @see LeaseAwareVaultPropertySource */ @CommonsLog -class LeasingVaultPropertySourceLocator extends VaultPropertySourceLocator - implements DisposableBean { +class LeasingVaultPropertySourceLocator extends VaultPropertySourceLocatorSupport + implements PriorityOrdered { - private final VaultConfigOperations operations; + private static final UriTemplateHandler TEMPLATE_HANDLER = new DefaultUriTemplateHandler(); + + private final SecretLeaseContainer secretLeaseContainer; private final VaultProperties properties; - private final TaskScheduler taskScheduler; - - private final Set> locatedPropertySources = new HashSet<>(); - /** * Creates a new {@link LeasingVaultPropertySourceLocator}. - * @param operations must not be {@literal null}. * @param properties must not be {@literal null}. * @param genericBackendProperties must not be {@literal null}. * @param backendAccessors must not be {@literal null}. - * @param taskScheduler must not be {@literal null}. + * @param secretLeaseContainer must not be {@literal null}. */ - public LeasingVaultPropertySourceLocator(VaultConfigOperations operations, - VaultProperties properties, + public LeasingVaultPropertySourceLocator(VaultProperties properties, VaultGenericBackendProperties genericBackendProperties, Collection backendAccessors, - TaskScheduler taskScheduler) { + SecretLeaseContainer secretLeaseContainer) { - super(operations, properties, genericBackendProperties, backendAccessors); + super("vault", genericBackendProperties, backendAccessors); - Assert.notNull(taskScheduler, "TaskScheduler must not be null"); - Assert.notNull(operations, "VaultConfigTemplate must not be null"); + Assert.notNull(secretLeaseContainer, "SecretLeaseContainer must not be null"); Assert.notNull(properties, "VaultProperties must not be null"); - this.operations = operations; + this.secretLeaseContainer = secretLeaseContainer; this.properties = properties; - this.taskScheduler = taskScheduler; } @Override - protected VaultPropertySource createVaultPropertySource( + public int getOrder() { + return properties.getConfig().getOrder(); + } + + /** + * Create {@link VaultPropertySource} initialized with a + * {@link SecretBackendMetadata}. + * + * @param accessor the {@link SecretBackendMetadata}. + * @return the {@link VaultPropertySource} to use. + */ + protected PropertySource createVaultPropertySource( SecretBackendMetadata accessor) { - LeasingVaultPropertySource propertySource = new LeasingVaultPropertySource( - this.operations, this.properties.isFailFast(), accessor, taskScheduler); + URI expand = TEMPLATE_HANDLER.expand("{backend}/{key}", accessor.getVariables()); - locatedPropertySources.add(propertySource); + final RequestedSecret secret = RequestedSecret.renewable(expand.getPath()); - return propertySource; + if (properties.isFailFast()) { + return createVaultPropertySourceFailFast(secret, accessor); + } + + return createVaultPropertySource(secret, accessor); } - @Override - public void destroy() { + /** + * Decorated {@link PropertySource} creation to catch and throw the first error that + * occurred durin initial secret retrieval. + * + * @param secret + * @param accessor + * @return + */ + private PropertySource createVaultPropertySourceFailFast( + final RequestedSecret secret, SecretBackendMetadata accessor) { - Set> propertySources = new HashSet<>(locatedPropertySources); + final AtomicReference errorRef = new AtomicReference<>(); - for (PropertySource propertySource : propertySources) { + LeaseErrorListener errorListener = new LeaseErrorListener() { + @Override + public void onLeaseError(SecretLeaseEvent leaseEvent, Exception exception) { - locatedPropertySources.remove(propertySource); - - if (propertySource instanceof LeasingVaultPropertySource) { - - try { - ((LeasingVaultPropertySource) propertySource).destroy(); + if (leaseEvent.getSource() == secret) { + errorRef.compareAndSet(null, exception); } - catch (Exception e) { - log.warn(String.format("Cannot destroy property source %s", - propertySource.getName()), e); + } + }; + + this.secretLeaseContainer.addErrorListener(errorListener); + try { + return createVaultPropertySource(secret, accessor); + } + finally { + this.secretLeaseContainer.removeLeaseErrorListener(errorListener); + + Exception exception = errorRef.get(); + if (exception != null) { + if (exception instanceof VaultException) { + throw (VaultException) exception; } + throw new VaultException( + String.format("Cannot initialize PropertySource for secret at %s", + secret.getPath()), + exception); } } } + + private PropertySource createVaultPropertySource(RequestedSecret secret, + SecretBackendMetadata accessor) { + + return new LeaseAwareVaultPropertySource(accessor.getName(), + this.secretLeaseContainer, secret, accessor.getPropertyTransformer()); + } } 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 be914e82..25ddc44d 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 @@ -26,9 +26,11 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 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.bootstrap.config.PropertySourceLocator; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.scheduling.TaskScheduler; @@ -56,10 +58,11 @@ import org.springframework.vault.authentication.StaticUserId; import org.springframework.vault.authentication.TokenAuthentication; import org.springframework.vault.client.VaultClients; import org.springframework.vault.client.VaultEndpoint; -import org.springframework.vault.config.ClientHttpRequestFactoryFactory; import org.springframework.vault.config.AbstractVaultConfiguration.ClientFactoryWrapper; +import org.springframework.vault.config.ClientHttpRequestFactoryFactory; import org.springframework.vault.core.VaultOperations; import org.springframework.vault.core.VaultTemplate; +import org.springframework.vault.core.lease.SecretLeaseContainer; import org.springframework.vault.support.ClientOptions; import org.springframework.vault.support.SslConfiguration; import org.springframework.vault.support.VaultToken; @@ -111,11 +114,11 @@ public class VaultBootstrapConfiguration implements InitializingBean { @SuppressWarnings("unchecked") public void afterPropertiesSet() throws Exception { - this.vaultSecretBackendDescriptors = applicationContext.getBeansOfType( - VaultSecretBackendDescriptor.class).values(); + this.vaultSecretBackendDescriptors = applicationContext + .getBeansOfType(VaultSecretBackendDescriptor.class).values(); - this.factories = (Collection) applicationContext.getBeansOfType( - SecretBackendMetadataFactory.class).values(); + this.factories = (Collection) applicationContext + .getBeansOfType(SecretBackendMetadataFactory.class).values(); ClientHttpRequestFactory clientHttpRequestFactory = clientHttpRequestFactoryWrapper() .getClientHttpRequestFactory(); @@ -125,11 +128,10 @@ public class VaultBootstrapConfiguration implements InitializingBean { } @Bean - public VaultPropertySourceLocator vaultPropertySourceLocator( - VaultOperations operations, + public PropertySourceLocator vaultPropertySourceLocator(VaultOperations operations, VaultProperties vaultProperties, VaultGenericBackendProperties vaultGenericBackendProperties, - ObjectFactory> taskSchedulerFactory) { + ObjectFactory secretLeaseContainerObjectFactory) { Collection backendAccessors = SecretBackendFactories .createSecretBackendMetadata(vaultSecretBackendDescriptors, factories); @@ -142,9 +144,13 @@ public class VaultBootstrapConfiguration implements InitializingBean { // otherwise, the bootstrap context is not shut down cleanly applicationContext.registerShutdownHook(); - return new LeasingVaultPropertySourceLocator(vaultConfigTemplate, - vaultProperties, vaultGenericBackendProperties, backendAccessors, - taskSchedulerFactory.getObject().getTaskScheduler()); + SecretLeaseContainer secretLeaseContainer = secretLeaseContainerObjectFactory + .getObject(); + secretLeaseContainer.start(); + + return new LeasingVaultPropertySourceLocator(vaultProperties, + vaultGenericBackendProperties, backendAccessors, + secretLeaseContainer); } return new VaultPropertySourceLocator(vaultConfigTemplate, vaultProperties, @@ -179,8 +185,8 @@ public class VaultBootstrapConfiguration implements InitializingBean { sslConfiguration = SslConfiguration.NONE; } - return new ClientFactoryWrapper(ClientHttpRequestFactoryFactory.create( - clientOptions, sslConfiguration)); + return new ClientFactoryWrapper( + ClientHttpRequestFactoryFactory.create(clientOptions, sslConfiguration)); } /** @@ -192,8 +198,9 @@ public class VaultBootstrapConfiguration implements InitializingBean { @Bean @ConditionalOnMissingBean public VaultTemplate vaultTemplate(SessionManager sessionManager) { - return new VaultTemplate(vaultEndpoint, clientHttpRequestFactoryWrapper() - .getClientHttpRequestFactory(), sessionManager); + return new VaultTemplate(vaultEndpoint, + clientHttpRequestFactoryWrapper().getClientHttpRequestFactory(), + sessionManager); } /** @@ -204,15 +211,20 @@ public class VaultBootstrapConfiguration implements InitializingBean { * @see ThreadPoolTaskScheduler */ @Bean + @Lazy @ConditionalOnMissingBean(TaskSchedulerWrapper.class) - public TaskSchedulerWrapper vaultTaskScheduler() { + public TaskSchedulerWrapper vaultTaskScheduler() { ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); threadPoolTaskScheduler.setPoolSize(2); threadPoolTaskScheduler.setDaemon(true); threadPoolTaskScheduler.setThreadNamePrefix("Spring-Cloud-Vault-"); - return new TaskSchedulerWrapper<>(threadPoolTaskScheduler); + // This is to destroy bootstrap resources + // otherwise, the bootstrap context is not shut down cleanly + applicationContext.registerShutdownHook(); + + return new TaskSchedulerWrapper(threadPoolTaskScheduler); } /** @@ -222,9 +234,8 @@ public class VaultBootstrapConfiguration implements InitializingBean { */ @Bean @ConditionalOnMissingBean - public SessionManager sessionManager( - ClientAuthentication clientAuthentication, - ObjectFactory> asyncTaskExecutorFactory) { + public SessionManager sessionManager(ClientAuthentication clientAuthentication, + ObjectFactory asyncTaskExecutorFactory) { if (vaultProperties.getConfig().getLifecycle().isEnabled()) { return new LifecycleAwareSessionManager(clientAuthentication, @@ -235,6 +246,20 @@ public class VaultBootstrapConfiguration implements InitializingBean { return new SimpleSessionManager(clientAuthentication); } + /** + * @return the {@link SessionManager} for Vault session management. + * @see SessionManager + * @see LifecycleAwareSessionManager + */ + @Bean + @Lazy + @ConditionalOnMissingBean + public SecretLeaseContainer secretLeaseContainer(VaultOperations vaultOperations, + TaskSchedulerWrapper taskSchedulerWrapper) { + return new SecretLeaseContainer(vaultOperations, + taskSchedulerWrapper.getTaskScheduler()); + } + @Bean @ConditionalOnMissingBean public ClientAuthentication clientAuthentication() { @@ -263,9 +288,9 @@ public class VaultBootstrapConfiguration implements InitializingBean { } - throw new UnsupportedOperationException(String.format( - "Client authentication %s not supported", - vaultProperties.getAuthentication())); + throw new UnsupportedOperationException( + String.format("Client authentication %s not supported", + vaultProperties.getAuthentication())); } private ClientAuthentication appIdAuthentication(VaultProperties vaultProperties) { @@ -300,8 +325,8 @@ public class VaultBootstrapConfiguration implements InitializingBean { if (StringUtils.hasText(appId.getNetworkInterface())) { try { - return new MacAddressUserId(Integer.parseInt(appId - .getNetworkInterface())); + return new MacAddressUserId( + Integer.parseInt(appId.getNetworkInterface())); } catch (NumberFormatException e) { return new MacAddressUserId(appId.getNetworkInterface()); @@ -360,33 +385,27 @@ public class VaultBootstrapConfiguration implements InitializingBean { /** * Wrapper to keep {@link TaskScheduler} local to Spring Cloud Vault. - * @param */ - public static class TaskSchedulerWrapper - implements InitializingBean, DisposableBean { + public static class TaskSchedulerWrapper implements InitializingBean, DisposableBean { - private final T taskScheduler; + private final ThreadPoolTaskScheduler taskScheduler; - public TaskSchedulerWrapper(T taskScheduler) { + public TaskSchedulerWrapper(ThreadPoolTaskScheduler taskScheduler) { this.taskScheduler = taskScheduler; } - T getTaskScheduler() { + ThreadPoolTaskScheduler getTaskScheduler() { return taskScheduler; } @Override public void destroy() throws Exception { - if (taskScheduler instanceof DisposableBean) { - ((DisposableBean) taskScheduler).destroy(); - } + taskScheduler.destroy(); } @Override public void afterPropertiesSet() throws Exception { - if (taskScheduler instanceof InitializingBean) { - ((InitializingBean) taskScheduler).afterPropertiesSet(); - } + taskScheduler.afterPropertiesSet(); } } } diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultPropertySource.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultPropertySource.java index b39fd302..bcd82078 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultPropertySource.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultPropertySource.java @@ -85,10 +85,6 @@ class VaultPropertySource extends EnumerablePropertySource backendAccessors; /** * Creates a new {@link VaultPropertySourceLocator}. - * + * * @param operations must not be {@literal null}. * @param properties must not be {@literal null}. * @param genericBackendProperties must not be {@literal null}. @@ -60,30 +50,13 @@ class VaultPropertySourceLocator implements PropertySourceLocator, PriorityOrder VaultGenericBackendProperties genericBackendProperties, Collection backendAccessors) { + super("vault", genericBackendProperties, backendAccessors); + Assert.notNull(operations, "VaultConfigOperations must not be null"); Assert.notNull(properties, "VaultProperties must not be null"); - Assert.notNull(backendAccessors, "BackendAccessors must not be null"); - Assert.notNull(genericBackendProperties, - "VaultGenericBackendProperties must not be null"); this.operations = operations; this.properties = properties; - this.backendAccessors = backendAccessors; - this.genericBackendProperties = genericBackendProperties; - } - - @Override - public PropertySource locate(Environment environment) { - - if (environment instanceof ConfigurableEnvironment) { - - CompositePropertySource propertySource = createCompositePropertySource( - (ConfigurableEnvironment) environment); - initialize(propertySource); - - return propertySource; - } - return null; } @Override @@ -91,88 +64,6 @@ class VaultPropertySourceLocator implements PropertySourceLocator, PriorityOrder return properties.getConfig().getOrder(); } - private List buildContexts(ConfigurableEnvironment env) { - - String appName = genericBackendProperties.getApplicationName(); - List profiles = Arrays.asList(env.getActiveProfiles()); - List contexts = new ArrayList<>(); - - String defaultContext = genericBackendProperties.getDefaultContext(); - addContext(contexts, defaultContext, profiles); - - for (String context : StringUtils.commaDelimitedListToSet(appName)) { - addContext(contexts, context, profiles); - } - - Collections.reverse(contexts); - return contexts; - } - - private void addContext(List contexts, String context, List profiles) { - if (StringUtils.hasText(context)) { - if (!contexts.contains(context)) { - contexts.add(context); - } - - addProfiles(contexts, context, profiles); - } - } - - private CompositePropertySource createCompositePropertySource( - ConfigurableEnvironment environment) { - - List> propertySources = new ArrayList<>(); - - if (genericBackendProperties.isEnabled()) { - - List contexts = buildContexts(environment); - for (String propertySourceContext : contexts) { - - if (StringUtils.hasText(propertySourceContext)) { - - VaultPropertySource vaultPropertySource = createVaultPropertySource( - create(genericBackendProperties.getBackend(), - propertySourceContext)); - - propertySources.add(vaultPropertySource); - } - } - } - - for (SecretBackendMetadata backendAccessor : backendAccessors) { - - VaultPropertySource vaultPropertySource = createVaultPropertySource( - backendAccessor); - propertySources.add(vaultPropertySource); - } - - return doCreateCompositePropertySource(propertySources); - } - - // ------------------------------------------------------------------------- - // Implementation hooks and helper methods - // ------------------------------------------------------------------------- - - /** - * Create a {@link CompositePropertySource} given a {@link List} of - * {@link PropertySource}s. - * - * @param propertySources the property sources. - * @return the {@link CompositePropertySource} to use. - */ - protected CompositePropertySource doCreateCompositePropertySource( - List> propertySources) { - - CompositePropertySource compositePropertySource = new CompositePropertySource( - "vault"); - - for (PropertySource propertySource : propertySources) { - compositePropertySource.addPropertySource(propertySource); - } - - return compositePropertySource; - } - /** * Initialize nested {@link PropertySource}s inside the * {@link CompositePropertySource}. @@ -192,22 +83,9 @@ class VaultPropertySourceLocator implements PropertySourceLocator, PriorityOrder * @param accessor the {@link SecretBackendMetadata}. * @return the {@link VaultPropertySource} to use. */ - protected VaultPropertySource createVaultPropertySource( + protected PropertySource createVaultPropertySource( SecretBackendMetadata accessor) { return new VaultPropertySource(this.operations, this.properties.isFailFast(), accessor); } - - private void addProfiles(List contexts, String baseContext, - List profiles) { - - for (String profile : profiles) { - String context = baseContext - + this.genericBackendProperties.getProfileSeparator() + profile; - - if (!contexts.contains(context)) { - contexts.add(context); - } - } - } } diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultPropertySourceLocatorSupport.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultPropertySourceLocatorSupport.java new file mode 100644 index 00000000..00a9e4a9 --- /dev/null +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultPropertySourceLocatorSupport.java @@ -0,0 +1,188 @@ +/* + * 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.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.springframework.cloud.bootstrap.config.PropertySourceLocator; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertySource; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import static org.springframework.cloud.vault.config.GenericSecretBackendMetadata.create; + +/** + * Abstract {@link PropertySourceLocator} to create {@link PropertySource}s based on + * {@link VaultGenericBackendProperties} and {@link SecretBackendMetadata}. + * + * @author Mark Paluch + */ +public abstract class VaultPropertySourceLocatorSupport implements PropertySourceLocator { + + private final String propertySourceName; + + private final VaultGenericBackendProperties genericBackendProperties; + + private final Collection backendAccessors; + + /** + * Creates a new {@link VaultPropertySourceLocatorSupport}. + * + * @param propertySourceName must not be {@literal null} or empty. + * @param genericBackendProperties must not be {@literal null}. + * @param backendAccessors must not be {@literal null}. + */ + public VaultPropertySourceLocatorSupport(String propertySourceName, + VaultGenericBackendProperties genericBackendProperties, + Collection backendAccessors) { + + Assert.hasText(propertySourceName, "PropertySource name must not be empty"); + Assert.notNull(backendAccessors, "BackendAccessors must not be null"); + Assert.notNull(genericBackendProperties, + "VaultGenericBackendProperties must not be null"); + + this.propertySourceName = propertySourceName; + this.backendAccessors = backendAccessors; + this.genericBackendProperties = genericBackendProperties; + } + + @Override + public PropertySource locate(Environment environment) { + + CompositePropertySource propertySource = createCompositePropertySource( + environment); + initialize(propertySource); + + return propertySource; + } + + // ------------------------------------------------------------------------- + // Implementation hooks and helper methods + // ------------------------------------------------------------------------- + + /** + * Allows initialization the {@link PropertySource} before use. Implementations may + * override this method to preload properties in the {@link PropertySource}. + * + * @param propertySource must not be {@literal null}. + */ + protected void initialize(CompositePropertySource propertySource) { + } + + /** + * Creates a {@link CompositePropertySource}. + * + * @param environment must not be {@literal null}. + * @return + */ + protected CompositePropertySource createCompositePropertySource( + Environment environment) { + + List> propertySources = doCreatePropertySources(environment); + + return doCreateCompositePropertySource(propertySourceName, propertySources); + } + + /** + * Create {@link PropertySource}s given {@link Environment} from the property + * configuration. + * + * @param environment must not be {@literal null}. + * @return a {@link List} of ordered {@link PropertySource}s. + */ + protected List> doCreatePropertySources(Environment environment) { + + List> propertySources = new ArrayList<>(); + + if (genericBackendProperties.isEnabled()) { + propertySources.addAll(doCreateGenericPropertySources(environment)); + } + + for (SecretBackendMetadata backendAccessor : backendAccessors) { + + PropertySource vaultPropertySource = createVaultPropertySource( + backendAccessor); + propertySources.add(vaultPropertySource); + } + + return propertySources; + } + + /** + * Create {@link PropertySource}s using the generic {@literal secret} backend. + * Property sources for the generic secret backend derive from the application name + * and active profiles to generate context paths. + * + * @param environment must not be {@literal null}. + * @return + */ + protected List> doCreateGenericPropertySources( + Environment environment) { + + List> propertySources = new ArrayList<>(); + List contexts = GenericSecretBackendMetadata + .buildContexts(genericBackendProperties, environment); + + for (String propertySourceContext : contexts) { + + if (StringUtils.hasText(propertySourceContext)) { + + PropertySource vaultPropertySource = createVaultPropertySource(create( + genericBackendProperties.getBackend(), propertySourceContext)); + + propertySources.add(vaultPropertySource); + } + } + + return propertySources; + } + + /** + * Create a {@link CompositePropertySource} given a {@link List} of + * {@link PropertySource}s. + * + * @param propertySourceName the property source name. + * @param propertySources the property sources. + * @return the {@link CompositePropertySource} to use. + */ + protected CompositePropertySource doCreateCompositePropertySource( + String propertySourceName, List> propertySources) { + + CompositePropertySource compositePropertySource = new CompositePropertySource( + propertySourceName); + + for (PropertySource propertySource : propertySources) { + compositePropertySource.addPropertySource(propertySource); + } + + return compositePropertySource; + } + + /** + * Create {@link VaultPropertySource} initialized with a + * {@link SecretBackendMetadata}. + * + * @param accessor the {@link SecretBackendMetadata}. + * @return the {@link VaultPropertySource} to use. + */ + protected abstract PropertySource createVaultPropertySource( + SecretBackendMetadata accessor); + +} diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/ApplicationFailFastTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/ApplicationFailFastTests.java index 5bb0cebc..2963c393 100644 --- a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/ApplicationFailFastTests.java +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/ApplicationFailFastTests.java @@ -16,13 +16,14 @@ package org.springframework.cloud.vault.config; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.fail; - import org.junit.Test; + import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; + /** * Tests for fail fast option. * @@ -32,10 +33,25 @@ import org.springframework.boot.builder.SpringApplicationBuilder; public class ApplicationFailFastTests { @Test - public void contextLoadsWithFailFast() { + public void contextLoadsWithFailFastUsingLeasing() { try { new SpringApplicationBuilder().sources(ApplicationFailFastTests.class).run( "--server.port=0", "--spring.cloud.vault.failFast=true", + "--spring.cloud.vault.config.lifecycle.enabled=true", + "--spring.cloud.vault.port=9999"); + fail("failFast option did not produce an exception"); + } + catch (Exception e) { + assertThat(e.getMessage()).isNotEmpty(); + } + } + + @Test + public void contextLoadsWithFailFastWithoutLeasing() { + try { + new SpringApplicationBuilder().sources(ApplicationFailFastTests.class).run( + "--server.port=0", "--spring.cloud.vault.failFast=true", + "--spring.cloud.vault.config.lifecycle.enabled=false", "--spring.cloud.vault.port=9999"); fail("failFast option did not produce an exception"); } diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceLocatorUnitTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceLocatorUnitTests.java index aee817ed..4daead1c 100644 --- a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceLocatorUnitTests.java +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceLocatorUnitTests.java @@ -15,11 +15,7 @@ */ package org.springframework.cloud.vault.config; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; - import java.util.Collections; -import java.util.Set; import org.junit.Before; import org.junit.Test; @@ -30,8 +26,10 @@ import org.mockito.runners.MockitoJUnitRunner; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.PropertySource; -import org.springframework.scheduling.TaskScheduler; -import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.vault.core.lease.SecretLeaseContainer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; /** * Unit tests for {@link LeasingVaultPropertySourceLocator}. @@ -43,24 +41,18 @@ public class LeasingVaultPropertySourceLocatorUnitTests { private LeasingVaultPropertySourceLocator propertySourceLocator; - @Mock - private VaultConfigTemplate operations; - - @Mock - private TaskScheduler taskScheduler; - @Mock private ConfigurableEnvironment configurableEnvironment; @Mock - private LeasingVaultPropertySource leasingVaultPropertySource; + private SecretLeaseContainer secretLeaseContainer; @Before public void before() { - propertySourceLocator = new LeasingVaultPropertySourceLocator(operations, + propertySourceLocator = new LeasingVaultPropertySourceLocator( new VaultProperties(), new VaultGenericBackendProperties(), - Collections. emptyList(), taskScheduler); + Collections.emptyList(), secretLeaseContainer); } @Test @@ -69,9 +61,9 @@ public class LeasingVaultPropertySourceLocatorUnitTests { VaultProperties vaultProperties = new VaultProperties(); vaultProperties.getConfig().setOrder(10); - propertySourceLocator = new LeasingVaultPropertySourceLocator(operations, - vaultProperties, new VaultGenericBackendProperties(), - Collections. emptyList(), taskScheduler); + propertySourceLocator = new LeasingVaultPropertySourceLocator(vaultProperties, + new VaultGenericBackendProperties(), + Collections.emptyList(), secretLeaseContainer); assertThat(propertySourceLocator.getOrder()).isEqualTo(10); } @@ -89,17 +81,4 @@ public class LeasingVaultPropertySourceLocatorUnitTests { CompositePropertySource composite = (CompositePropertySource) propertySource; assertThat(composite.getPropertySources()).hasSize(1); } - - @Test - @SuppressWarnings("unchecked") - public void shouldDispose() { - - Set set = (Set) ReflectionTestUtils.getField(propertySourceLocator, - "locatedPropertySources"); - set.add(leasingVaultPropertySource); - - propertySourceLocator.destroy(); - - verify(leasingVaultPropertySource).destroy(); - } -} \ No newline at end of file +} diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceUnitTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceUnitTests.java deleted file mode 100644 index 39ee9c00..00000000 --- a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceUnitTests.java +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Copyright 2016-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.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.scheduling.TaskScheduler; -import org.springframework.scheduling.Trigger; -import org.springframework.vault.core.RestOperationsCallback; -import org.springframework.vault.core.VaultOperations; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; - -/** - * Unit tests for {@link LeasingVaultPropertySource}. - * - * @author Mark Paluch - */ -@RunWith(MockitoJUnitRunner.class) -public class LeasingVaultPropertySourceUnitTests { - - @Mock - private VaultConfigTemplate configOperations; - - @Mock - private VaultOperations vaultOperations; - - @Mock - private SecretBackendMetadata secretBackendMetadata; - - @Mock - private TaskScheduler taskScheduler; - - @Mock - private ScheduledFuture scheduledFuture; - - private LeasingVaultPropertySource propertySource; - - @Before - public void before() throws Exception { - - when(secretBackendMetadata.getName()).thenReturn("test"); - when(configOperations.getVaultOperations()).thenReturn(vaultOperations); - - propertySource = new LeasingVaultPropertySource(configOperations, false, - secretBackendMetadata, taskScheduler); - } - - @Test - public void shouldWorkIfSecretsNotFound() { - - propertySource.init(); - - assertThat(propertySource.getPropertyNames()).isEmpty(); - } - - @Test - public void shouldAcceptSecretsWithoutLease() { - - Secrets secrets = new Secrets(); - secrets.setData(Collections.singletonMap("key", "value")); - - when(configOperations.read(secretBackendMetadata)).thenReturn(secrets); - - propertySource.init(); - - assertThat(propertySource.getPropertyNames()).contains("key"); - verifyZeroInteractions(taskScheduler); - } - - @Test - public void shouldAcceptSecretsWithStaticLease() { - - Secrets secrets = new Secrets(); - secrets.setLeaseId("lease"); - secrets.setRenewable(false); - secrets.setData(Collections.singletonMap("key", "value")); - - when(configOperations.read(secretBackendMetadata)).thenReturn(secrets); - - propertySource.init(); - - verifyZeroInteractions(taskScheduler); - } - - @Test - @SuppressWarnings("unchecked") - public void shouldAcceptSecretsWithRenewableLease() { - - when(taskScheduler.schedule(any(Runnable.class), any(Trigger.class))).thenReturn( - scheduledFuture); - when(configOperations.read(secretBackendMetadata)).thenReturn(createSecrets()); - - propertySource.init(); - - verify(taskScheduler).schedule(any(Runnable.class), any(Trigger.class)); - } - - @Test - @SuppressWarnings("unchecked") - public void shouldRenewLease() { - - prepareRenewal(); - - ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); - verify(taskScheduler).schedule(captor.capture(), any(Trigger.class)); - - captor.getValue().run(); - verifyZeroInteractions(scheduledFuture); - verify(taskScheduler, times(2)).schedule(captor.capture(), any(Trigger.class)); - } - - @Test - @SuppressWarnings("unchecked") - public void scheduleRenewalShouldApplyExpiryThreshold() { - - prepareRenewal(); - - ArgumentCaptor captor = ArgumentCaptor.forClass(Trigger.class); - verify(taskScheduler).schedule(any(Runnable.class), captor.capture()); - - Date nextExecutionTime = captor.getValue().nextExecutionTime(null); - assertThat(nextExecutionTime).isBetween( - new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(35)), - new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(41))); - } - - @Test - @SuppressWarnings("unchecked") - public void subsequentScheduleRenewalShouldApplyExpiryThreshold() { - - prepareRenewal(); - - ArgumentCaptor runnableCaptor = ArgumentCaptor.forClass(Runnable.class); - verify(taskScheduler).schedule(runnableCaptor.capture(), any(Trigger.class)); - - runnableCaptor.getValue().run(); - - ArgumentCaptor captor = ArgumentCaptor.forClass(Trigger.class); - verify(taskScheduler, times(2)).schedule(any(Runnable.class), captor.capture()); - - assertThat(captor.getAllValues().get(0).nextExecutionTime(null)).isBetween( - new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(35)), - new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(41))); - - assertThat(captor.getAllValues().get(1).nextExecutionTime(null)).isBetween( - new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(9)), - new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(11))); - } - - @Test - @SuppressWarnings("unchecked") - public void scheduleRenewalShouldTriggerOnlyOnce() { - - prepareRenewal(); - - ArgumentCaptor captor = ArgumentCaptor.forClass(Trigger.class); - verify(taskScheduler).schedule(any(Runnable.class), captor.capture()); - - Trigger trigger = captor.getValue(); - - assertThat(trigger.nextExecutionTime(null)).isNotNull(); - assertThat(trigger.nextExecutionTime(null)).isNull(); - } - - @Test - @SuppressWarnings("unchecked") - public void subsequentInitShouldCancelExistingSchedule() { - - prepareRenewal(); - - ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); - verify(taskScheduler).schedule(captor.capture(), any(Trigger.class)); - - propertySource.init(); - - verify(scheduledFuture).cancel(false); - verify(taskScheduler, times(2)).schedule(captor.capture(), any(Trigger.class)); - } - - @Test - @SuppressWarnings("unchecked") - public void canceledRenewalShouldSkipRenewal() { - - prepareRenewal(); - - ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); - verify(taskScheduler).schedule(captor.capture(), any(Trigger.class)); - - propertySource.init(); - verify(scheduledFuture).cancel(false); - - captor.getValue().run(); - - verifyZeroInteractions(vaultOperations); - } - - @Test - public void shouldDisableRenewalOnDisposal() { - - prepareRenewal(); - - propertySource.destroy(); - - verify(vaultOperations).doWithSession(any(RestOperationsCallback.class)); - verify(scheduledFuture).cancel(false); - } - - private void prepareRenewal() { - - when(taskScheduler.schedule(any(Runnable.class), any(Trigger.class))).thenReturn( - scheduledFuture); - when(configOperations.read(secretBackendMetadata)).thenReturn(createSecrets()); - propertySource.init(); - when(vaultOperations.doWithSession(any(RestOperationsCallback.class))) - .thenReturn(getResponseEntity("new_lease", true, 70, HttpStatus.OK)); - } - - private ResponseEntity> getResponseEntity(String leaseId, - Boolean renewable, Integer leaseDuration, HttpStatus httpStatus) { - - Map body = new HashMap<>(); - body.put("lease_id", leaseId); - body.put("renewable", renewable); - body.put("lease_duration", leaseDuration); - - return getEntity(body, httpStatus); - } - - private ResponseEntity> getEntity(Map body, - HttpStatus status) { - - return new ResponseEntity<>(body, status); - } - - private Secrets createSecrets() { - - Secrets secrets = new Secrets(); - - secrets.setLeaseId("lease"); - secrets.setRenewable(true); - secrets.setLeaseDuration(100); - secrets.setData(Collections.singletonMap("key", "value")); - - return secrets; - } -} diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigTests.java index 4c47acdb..a640fb49 100644 --- a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigTests.java +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigTests.java @@ -95,7 +95,7 @@ public class VaultConfigTests { ApplicationContext parent = applicationContext.getParent(); assertThat(parent.getBeanNamesForType(VaultTemplate.class)).isNotEmpty(); - assertThat(parent.getBeanNamesForType(VaultPropertySourceLocator.class)) + assertThat(parent.getBeanNamesForType(LeasingVaultPropertySourceLocator.class)) .isNotEmpty(); }