Adopt Spring Vault's LeaseAwareVaultPropertySource.

We now use Spring Vault's LeaseAwareVaultPropertySource instead of the own `LeaseAwareVaultPropertySource`.

Closes gh-88.
This commit is contained in:
Mark Paluch
2017-03-09 17:58:53 +02:00
parent bee9e716e2
commit 754ae1eee7
12 changed files with 426 additions and 1007 deletions

View File

@@ -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<String> buildContexts(
VaultGenericBackendProperties genericBackendProperties,
Environment environment) {
String appName = genericBackendProperties.getApplicationName();
List<String> profiles = Arrays.asList(environment.getActiveProfiles());
List<String> 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<String> contexts, String applicationName,
List<String> 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);
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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}.
*
* <p>
* {@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<Map<String, Object>> entity = null;
try {
entity = getSource().getVaultOperations().doWithSession(
new RestOperationsCallback<ResponseEntity<Map<String, Object>>>() {
@Override
@SuppressWarnings("unchecked")
public ResponseEntity<Map<String, Object>> doWithRestOperations(
RestOperations restOperations) {
return (ResponseEntity) restOperations.exchange(
"/sys/renew/{leaseId}", HttpMethod.PUT, null,
Map.class, lease.getLeaseId());
}
});
Map<String, Object> 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<ResponseEntity<Map<String, Object>>>() {
@Override
@SuppressWarnings("unchecked")
public ResponseEntity<Map<String, Object>> 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<Lease> currentLease = new AtomicReference<>();
private final Map<Lease, ScheduledFuture<?>> 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<Lease> 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;
}
}

View File

@@ -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<PropertySource<?>> 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<SecretBackendMetadata> 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<PropertySource<?>> propertySources = new HashSet<>(locatedPropertySources);
final AtomicReference<Exception> 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());
}
}

View File

@@ -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<TaskSchedulerWrapper<? extends TaskScheduler>> taskSchedulerFactory) {
ObjectFactory<SecretLeaseContainer> secretLeaseContainerObjectFactory) {
Collection<SecretBackendMetadata> 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<ThreadPoolTaskScheduler> 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<TaskSchedulerWrapper<? extends AsyncTaskExecutor>> asyncTaskExecutorFactory) {
public SessionManager sessionManager(ClientAuthentication clientAuthentication,
ObjectFactory<TaskSchedulerWrapper> 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 <T>
*/
public static class TaskSchedulerWrapper<T extends AsyncTaskExecutor & TaskScheduler>
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();
}
}
}

View File

@@ -85,10 +85,6 @@ class VaultPropertySource extends EnumerablePropertySource<VaultConfigOperations
}
}
Secrets getSecrets() {
return secrets;
}
@Override
public Object getProperty(String name) {
return this.properties.get(name);

View File

@@ -15,22 +15,13 @@
*/
package org.springframework.cloud.vault.config;
import static org.springframework.cloud.vault.config.GenericSecretBackendMetadata.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@link PropertySourceLocator} using {@link VaultConfigTemplate}.
@@ -40,16 +31,15 @@ import org.springframework.util.StringUtils;
* @author Jean-Philippe Bélanger
* @author Ryan Hoegg
*/
class VaultPropertySourceLocator implements PropertySourceLocator, PriorityOrdered {
class VaultPropertySourceLocator extends VaultPropertySourceLocatorSupport
implements PriorityOrdered {
private final VaultConfigOperations operations;
private final VaultProperties properties;
private final VaultGenericBackendProperties genericBackendProperties;
private final Collection<SecretBackendMetadata> 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<SecretBackendMetadata> 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<String> buildContexts(ConfigurableEnvironment env) {
String appName = genericBackendProperties.getApplicationName();
List<String> profiles = Arrays.asList(env.getActiveProfiles());
List<String> 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<String> contexts, String context, List<String> profiles) {
if (StringUtils.hasText(context)) {
if (!contexts.contains(context)) {
contexts.add(context);
}
addProfiles(contexts, context, profiles);
}
}
private CompositePropertySource createCompositePropertySource(
ConfigurableEnvironment environment) {
List<PropertySource<?>> propertySources = new ArrayList<>();
if (genericBackendProperties.isEnabled()) {
List<String> 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<PropertySource<?>> 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<String> contexts, String baseContext,
List<String> profiles) {
for (String profile : profiles) {
String context = baseContext
+ this.genericBackendProperties.getProfileSeparator() + profile;
if (!contexts.contains(context)) {
contexts.add(context);
}
}
}
}

View File

@@ -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<SecretBackendMetadata> 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<SecretBackendMetadata> 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<PropertySource<?>> 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<PropertySource<?>> doCreatePropertySources(Environment environment) {
List<PropertySource<?>> 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<PropertySource<?>> doCreateGenericPropertySources(
Environment environment) {
List<PropertySource<?>> propertySources = new ArrayList<>();
List<String> 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<PropertySource<?>> 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);
}

View File

@@ -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");
}

View File

@@ -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.<SecretBackendMetadata> emptyList(), taskScheduler);
Collections.<SecretBackendMetadata>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.<SecretBackendMetadata> emptyList(), taskScheduler);
propertySourceLocator = new LeasingVaultPropertySourceLocator(vaultProperties,
new VaultGenericBackendProperties(),
Collections.<SecretBackendMetadata>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();
}
}
}

View File

@@ -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<Runnable> 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<Trigger> 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<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
verify(taskScheduler).schedule(runnableCaptor.capture(), any(Trigger.class));
runnableCaptor.getValue().run();
ArgumentCaptor<Trigger> 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<Trigger> 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<Runnable> 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<Runnable> 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<Map<String, Object>> getResponseEntity(String leaseId,
Boolean renewable, Integer leaseDuration, HttpStatus httpStatus) {
Map<String, Object> body = new HashMap<>();
body.put("lease_id", leaseId);
body.put("renewable", renewable);
body.put("lease_duration", leaseDuration);
return getEntity(body, httpStatus);
}
private ResponseEntity<Map<String, Object>> getEntity(Map<String, Object> 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;
}
}

View File

@@ -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();
}