Use events to initiate ConfigurationProperties refresh in the application context

The ConsulBackendMetadata basically publishes and event on SecretLeaseCreatedEvent and something else does the rebinding

See gh-393
This commit is contained in:
Spencer Gibb
2020-03-19 15:36:14 -04:00
committed by Mark Paluch
parent 8adc95e64e
commit 620b483ddd
8 changed files with 188 additions and 55 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2013-2020 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.
@@ -22,9 +22,9 @@ import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder;
import org.springframework.cloud.vault.config.LeasingSecretBackendMetadata;
import org.springframework.cloud.vault.config.PropertyNameTransformer;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.vault.core.lease.SecretLeaseContainer;
import org.springframework.vault.core.lease.domain.RequestedSecret;
import org.springframework.vault.core.lease.event.SecretLeaseCreatedEvent;
@@ -35,17 +35,19 @@ import org.springframework.vault.core.util.PropertyTransformer;
*/
class ConsulBackendMetadata implements LeasingSecretBackendMetadata {
private final Log log = LogFactory
.getLog(getClass());
private final Log log = LogFactory.getLog(getClass());
private final VaultConsulProperties properties;
private final PropertyNameTransformer transformer;
private final ConfigurationPropertiesRebinder rebinder;
public ConsulBackendMetadata(VaultConsulProperties properties, PropertyNameTransformer transformer, ConfigurationPropertiesRebinder rebinder) {
private final PropertyTransformer transformer;
private final ApplicationEventPublisher eventPublisher;
ConsulBackendMetadata(VaultConsulProperties properties,
PropertyTransformer transformer, ApplicationEventPublisher eventPublisher) {
this.properties = properties;
this.transformer = transformer;
this.rebinder = rebinder;
this.eventPublisher = eventPublisher;
}
@Override
@@ -66,8 +68,7 @@ class ConsulBackendMetadata implements LeasingSecretBackendMetadata {
Map<String, String> variables = new HashMap<>();
variables.put("backend", this.properties.getBackend());
variables
.put("key", String.format("creds/%s", this.properties.getRole()));
variables.put("key", String.format("creds/%s", this.properties.getRole()));
return variables;
}
@@ -83,28 +84,30 @@ class ConsulBackendMetadata implements LeasingSecretBackendMetadata {
}
@Override
public void afterRegistration(RequestedSecret secret, SecretLeaseContainer container) {
public void afterRegistration(RequestedSecret secret,
SecretLeaseContainer container) {
container.addLeaseListener(leaseEvent -> {
if (leaseEvent
.getSource() == secret && leaseEvent instanceof SecretLeaseCreatedEvent) {
rebind("consulDiscoveryProperties");
rebind("consulConfigProperties");
if (leaseEvent.getSource() == secret
&& leaseEvent instanceof SecretLeaseCreatedEvent) {
if (this.eventPublisher != null) {
if (log.isDebugEnabled()) {
log.debug("Publishing a RebindConsulEvent");
}
this.eventPublisher.publishEvent(new RebindConsulEvent(this));
}
}
});
// initial rebind after requesting these properties
rebind("consulDiscoveryProperties");
rebind("consulConfigProperties");
// no need to rebind here since the transformer creats all appropriate properties.
}
private void rebind(String bean) {
public static class RebindConsulEvent extends ApplicationEvent {
boolean success = this.rebinder.rebind(bean);
if (this.log.isInfoEnabled()) {
this.log.info(String
.format("Attempted to rebind Consul bean '%s' with updated ACL token from vault, success: %s", bean, success));
RebindConsulEvent(Object source) {
super(source);
}
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2016-2020 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
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.vault.config.consul;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder;
import org.springframework.cloud.vault.config.SecretBackendMetadataFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Bootstrap configuration providing support for the Consul secret backend.
*
* @author Mark Paluch
*/
@Configuration(proxyBeanMethods = false)
public class VaultConfigConsulAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public ConsulSecretRebindListener consulSecretRebindListener(
ConfigurationPropertiesRebinder rebinder,
ConfigurableApplicationContext context) {
// TODO: some other way? Maybe a BootstrapApplicationContextHolder bean
// provided by spring cloud commons
ApplicationContext parent = context.getParent();
if (parent != null) {
context = (ConfigurableApplicationContext) parent;
}
return new ConsulSecretRebindListener(rebinder, context);
}
/**
* {@link SecretBackendMetadataFactory} for Consul integration using
* {@link VaultConsulProperties}.
*/
public static class ConsulSecretRebindListener
implements ApplicationListener<ConsulBackendMetadata.RebindConsulEvent> {
private final Log log = LogFactory.getLog(getClass());
private final ConfigurationPropertiesRebinder rebinder;
public ConsulSecretRebindListener(ConfigurationPropertiesRebinder rebinder,
ConfigurableApplicationContext context) {
this.rebinder = rebinder;
context.addApplicationListener(this);
}
@Override
public void onApplicationEvent(ConsulBackendMetadata.RebindConsulEvent event) {
if (log.isDebugEnabled()) {
log.debug("received RebindConsulEvent");
}
rebind("consulDiscoveryProperties");
rebind("consulConfigProperties");
}
private void rebind(String bean) {
boolean success = this.rebinder.rebind(bean);
if (this.log.isInfoEnabled()) {
this.log.info(String.format(
"Attempted to rebind Consul bean '%s' with updated ACL token from vault, success: %s",
bean, success));
}
}
}
}

View File

@@ -16,19 +16,19 @@
package org.springframework.cloud.vault.config.consul;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder;
import org.springframework.cloud.vault.config.PropertyNameTransformer;
import org.springframework.cloud.vault.config.SecretBackendMetadata;
import org.springframework.cloud.vault.config.SecretBackendMetadataFactory;
import org.springframework.cloud.vault.config.VaultSecretBackendDescriptor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.Assert;
import org.springframework.vault.core.util.PropertyTransformer;
/**
* Bootstrap configuration providing support for the Consul secret backend.
@@ -41,8 +41,9 @@ public class VaultConfigConsulBootstrapConfiguration {
@Bean
@ConditionalOnMissingBean
public ConsulSecretBackendMetadataFactory consulSecretBackendAccessorFactory(ConfigurationPropertiesRebinder rebinder) {
return new ConsulSecretBackendMetadataFactory(rebinder);
public ConsulSecretBackendMetadataFactory consulSecretBackendMetadataFactory(
ApplicationContext context) {
return new ConsulSecretBackendMetadataFactory(context);
}
/**
@@ -52,10 +53,14 @@ public class VaultConfigConsulBootstrapConfiguration {
public static class ConsulSecretBackendMetadataFactory
implements SecretBackendMetadataFactory<VaultConsulProperties> {
private final ConfigurationPropertiesRebinder rebinder;
private final ApplicationContext context;
public ConsulSecretBackendMetadataFactory(ConfigurationPropertiesRebinder rebinder) {
this.rebinder = rebinder;
public ConsulSecretBackendMetadataFactory(ApplicationContext context) {
this.context = context;
}
public ApplicationContext getContext() {
return this.context;
}
/**
@@ -69,10 +74,26 @@ public class VaultConfigConsulBootstrapConfiguration {
Assert.notNull(properties, "VaultConsulProperties must not be null");
PropertyNameTransformer transformer = new PropertyNameTransformer();
transformer.addKeyTransformation("token", properties.getTokenProperty());
/*
* PropertyNameTransformer transformer = new PropertyNameTransformer();
* transformer.addKeyTransformation("token", properties.getTokenProperty());
*/
return new ConsulBackendMetadata(properties, transformer, this.rebinder);
// spring.cloud.consul.token is a shortcut for initialization
// for this I couldn't get it to work without specifiying each property
// specifically
PropertyTransformer transformer = input -> {
Map<String, Object> transformed = new LinkedHashMap<>();
transformed.put("spring.cloud.consul.config.acl-token",
input.get("token"));
transformed.put("spring.cloud.consul.discovery.acl-token",
input.get("token"));
return transformed;
};
return new ConsulBackendMetadata(properties, transformer, context);
}
@Override

View File

@@ -1,3 +1,6 @@
# Bootstrap Configuration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.vault.config.consul.VaultConfigConsulBootstrapConfiguration
org.springframework.cloud.vault.config.consul.VaultConfigConsulBootstrapConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.vault.config.consul.VaultConfigConsulAutoConfiguration

View File

@@ -131,7 +131,8 @@ public class ConsulSecretIntegrationTests extends IntegrationTestSupport {
@Test
public void shouldCreateCredentialsCorrectly() {
ConsulSecretBackendMetadataFactory factory = new ConsulSecretBackendMetadataFactory(null);
ConsulSecretBackendMetadataFactory factory = new ConsulSecretBackendMetadataFactory(
null);
Map<String, Object> secretProperties = this.configOperations
.read(factory.forConsul(this.consul)).getData();

View File

@@ -69,7 +69,7 @@ public class VaultConfigConsulBootstrapConfigurationTests extends IntegrationTes
@ConditionalOnProperty("VaultConfigConsulBootstrapConfigurationTests.custom.config")
ConsulSecretBackendMetadataFactory customFactory(ConfigurationPropertiesRebinder rebinder) {
return new ConsulSecretBackendMetadataFactory(rebinder) {
return new ConsulSecretBackendMetadataFactory(null) {
@Override
public SecretBackendMetadata createMetadata(
VaultConsulProperties backendDescriptor) {

View File

@@ -31,10 +31,12 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.consul.config.ConsulConfigProperties;
import org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties;
import org.springframework.cloud.vault.util.CanConnect;
import org.springframework.cloud.vault.util.VaultRule;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@@ -58,8 +60,9 @@ import static org.junit.Assume.assumeTrue;
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = VaultConfigConsulTests.TestApplication.class,
properties = {"spring.cloud.vault.consul.enabled=true",
"spring.cloud.vault.consul.role=readonly"})
properties = { "spring.cloud.vault.consul.enabled=true",
"spring.cloud.vault.consul.role=readonly",
"spring.cloud.consul.discovery.catalog-services-watch.enabled=false" })
public class VaultConfigConsulTests {
private static final String CONSUL_HOST = "localhost";
@@ -76,12 +79,21 @@ public class VaultConfigConsulTests {
private static final ParameterizedTypeReference<Map<String, String>> STRING_MAP = new ParameterizedTypeReference<Map<String, String>>() {
};
@Value("${spring.cloud.consul.token}")
String token;
@Value("${spring.cloud.consul.discovery.acl-token:}")
String discoveryToken;
@Value("${spring.cloud.consul.config.acl-token:}")
String configToken;
@Autowired
Environment env;
@Autowired
ConsulDiscoveryProperties discoveryProperties;
@Autowired
ConsulConfigProperties configProperties;
/**
* Initialize the consul secret backend.
*/
@@ -132,22 +144,25 @@ public class VaultConfigConsulTests {
}
}
@Test
public void shouldHaveToken() {
assertThat(this.token).isNotEmpty();
assertThat(this.discoveryProperties.getAclToken()).isEqualTo(this.token);
}
/*
* @Test public void shouldHaveToken() { assertThat(this.token).isNotEmpty();
* assertThat(this.discoveryProperties.getAclToken()).isEqualTo(this.token); }
*/
@Test
public void shouldHaveRenewedToken() throws InterruptedException {
assertThat(this.token).isNotEmpty();
assertThat(this.discoveryProperties.getAclToken()).isEqualTo(this.token);
assertThat(configToken).isNotEmpty();
assertThat(discoveryToken).isNotEmpty();
assertThat(this.configProperties.getAclToken()).isEqualTo(configToken);
assertThat(this.discoveryProperties.getAclToken()).isEqualTo(discoveryToken);
Thread.sleep(20_000L);
// TODO: The properties weren't rebound so this test fails.
assertThat(this.discoveryProperties.getAclToken()).isNotEmpty().isNotEqualTo(this.token);
assertThat(this.configProperties.getAclToken()).isNotEmpty()
.isNotEqualTo(configToken);
assertThat(this.discoveryProperties.getAclToken()).isNotEmpty()
.isNotEqualTo(discoveryToken);
}
@SpringBootApplication

View File

@@ -1,4 +1,3 @@
# Bootstrap Configuration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.vault.config.consul.VaultConfigConsulBootstrapConfigurationTests.CustomBootstrapConfiguration,\
org.springframework.cloud.vault.config.consul.VaultConfigConsulBootstrapConfiguration
org.springframework.cloud.vault.config.consul.VaultConfigConsulBootstrapConfigurationTests.CustomBootstrapConfiguration