Adds support for spring.config.import=consul: (#672)

This commit is contained in:
Spencer Gibb
2020-09-09 16:16:52 -04:00
committed by GitHub
parent e94f45663a
commit 989485a4e5
12 changed files with 697 additions and 112 deletions

View File

@@ -127,85 +127,88 @@ public class ConfigWatch implements ApplicationEventPublisherAware, SmartLifecyc
@Timed("consul.watch-config-keys")
public void watchConfigKeyValues() {
if (this.running.get()) {
for (String context : this.consulIndexes.keySet()) {
if (!this.running.get()) {
return;
}
for (String context : this.consulIndexes.keySet()) {
// turn the context into a Consul folder path (unless our config format
// are FILES)
if (this.properties.getFormat() != FILES && !context.endsWith("/")) {
context = context + "/";
// turn the context into a Consul folder path (unless our config format
// are FILES)
if (this.properties.getFormat() != FILES && !context.endsWith("/")) {
context = context + "/";
}
try {
Long currentIndex = this.consulIndexes.get(context);
if (currentIndex == null) {
currentIndex = -1L;
}
try {
Long currentIndex = this.consulIndexes.get(context);
if (currentIndex == null) {
currentIndex = -1L;
}
if (log.isTraceEnabled()) {
log.trace("watching consul for context '" + context + "' with index "
+ currentIndex);
}
// use the consul ACL token if found
String aclToken = this.properties.getAclToken();
if (StringUtils.isEmpty(aclToken)) {
aclToken = null;
}
// use the consul ACL token if found
String aclToken = this.properties.getAclToken();
if (StringUtils.isEmpty(aclToken)) {
aclToken = null;
}
Response<List<GetValue>> response = this.consul.getKVValues(context,
aclToken,
new QueryParams(this.properties.getWatch().getWaitTime(),
currentIndex));
Response<List<GetValue>> response = this.consul.getKVValues(context,
aclToken, new QueryParams(
this.properties.getWatch().getWaitTime(), currentIndex));
// if response.value == null, response was a 404, otherwise it was a
// 200
// reducing churn if there wasn't anything
if (response.getValue() != null && !response.getValue().isEmpty()) {
Long newIndex = response.getConsulIndex();
// if response.value == null, response was a 404, otherwise it was a
// 200, reducing churn if there wasn't anything
if (response.getValue() != null && !response.getValue().isEmpty()) {
Long newIndex = response.getConsulIndex();
if (newIndex != null && !newIndex.equals(currentIndex)) {
// don't publish the same index again, don't publish the first
// time (-1) so index can be primed
if (!this.consulIndexes.containsValue(newIndex)
&& !currentIndex.equals(-1L)) {
if (newIndex != null && !newIndex.equals(currentIndex)) {
// don't publish the same index again, don't publish the first
// time (-1) so index can be primed
if (!this.consulIndexes.containsValue(newIndex)
&& !currentIndex.equals(-1L)) {
if (log.isTraceEnabled()) {
log.trace("Context " + context + " has new index "
+ newIndex);
RefreshEventData data = new RefreshEventData(context,
currentIndex, newIndex);
this.publisher.publishEvent(
new RefreshEvent(this, data, data.toString()));
}
else if (log.isTraceEnabled()) {
log.trace("Event for index already published for context "
+ context);
}
this.consulIndexes.put(context, newIndex);
RefreshEventData data = new RefreshEventData(context,
currentIndex, newIndex);
this.publisher.publishEvent(
new RefreshEvent(this, data, data.toString()));
}
else if (log.isTraceEnabled()) {
log.trace("Same index for context " + context);
log.trace("Event for index already published for context "
+ context);
}
this.consulIndexes.put(context, newIndex);
}
else if (log.isTraceEnabled()) {
log.trace("No value for context " + context);
log.trace("Same index for context " + context);
}
}
catch (Exception e) {
// only fail fast on the initial query, otherwise just log the error
if (this.firstTime && this.properties.isFailFast()) {
log.error(
"Fail fast is set and there was an error reading configuration from consul.");
ReflectionUtils.rethrowRuntimeException(e);
}
else if (log.isTraceEnabled()) {
log.trace("Error querying consul Key/Values for context '"
+ context + "'", e);
}
else if (log.isWarnEnabled()) {
// simplified one line log message in the event of an agent
// failure
log.warn("Error querying consul Key/Values for context '"
+ context + "'. Message: " + e.getMessage());
}
else if (log.isTraceEnabled()) {
log.trace("No value for context " + context);
}
}
catch (Exception e) {
// only fail fast on the initial query, otherwise just log the error
if (this.firstTime && this.properties.isFailFast()) {
log.error(
"Fail fast is set and there was an error reading configuration from consul.");
ReflectionUtils.rethrowRuntimeException(e);
}
else if (log.isTraceEnabled()) {
log.trace("Error querying consul Key/Values for context '" + context
+ "'", e);
}
else if (log.isWarnEnabled()) {
// simplified one line log message in the event of an agent
// failure
log.warn("Error querying consul Key/Values for context '" + context
+ "'. Message: " + e.getMessage());
}
}
}

View File

@@ -19,8 +19,11 @@ package org.springframework.cloud.consul.config;
import com.ecwid.consul.v1.ConsulClient;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
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.consul.ConditionalOnConsulEnabled;
import org.springframework.cloud.endpoint.RefreshEndpoint;
import org.springframework.context.annotation.Bean;
@@ -34,6 +37,7 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
@Configuration(proxyBeanMethods = false)
@ConditionalOnConsulEnabled
@ConditionalOnProperty(name = "spring.cloud.consul.config.enabled", matchIfMissing = true)
@EnableConfigurationProperties
public class ConsulConfigAutoConfiguration {
/**
@@ -41,23 +45,28 @@ public class ConsulConfigAutoConfiguration {
*/
public static final String CONFIG_WATCH_TASK_SCHEDULER_NAME = "configWatchTaskScheduler";
@Bean
@ConditionalOnMissingBean
public ConsulConfigProperties consulConfigProperties() {
return new ConsulConfigProperties();
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RefreshEndpoint.class)
@ConditionalOnProperty(name = "spring.cloud.consul.config.watch.enabled",
matchIfMissing = true)
protected static class ConsulRefreshConfiguration {
@Bean
@ConditionalOnProperty(name = "spring.cloud.consul.config.watch.enabled",
matchIfMissing = true)
@ConditionalOnBean(ConsulConfigIndexes.class)
public ConfigWatch configWatch(ConsulConfigProperties properties,
ConsulPropertySourceLocator locator, ConsulClient consul,
ConsulConfigIndexes indexes, ConsulClient consul,
@Qualifier(CONFIG_WATCH_TASK_SCHEDULER_NAME) TaskScheduler taskScheduler) {
return new ConfigWatch(properties, consul, locator.getContextIndexes(),
return new ConfigWatch(properties, consul, indexes.getIndexes(),
taskScheduler);
}
@Bean(name = CONFIG_WATCH_TASK_SCHEDULER_NAME)
@ConditionalOnProperty(name = "spring.cloud.consul.config.watch.enabled",
matchIfMissing = true)
public TaskScheduler configWatchTaskScheduler() {
return new ThreadPoolTaskScheduler();
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2015-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.consul.config;
import java.util.Collections;
import com.ecwid.consul.v1.ConsulClient;
import com.ecwid.consul.v1.Response;
import com.ecwid.consul.v1.kv.model.GetValue;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.context.config.ConfigData;
import org.springframework.boot.context.config.ConfigDataLoader;
import org.springframework.boot.context.config.ConfigDataLoaderContext;
import org.springframework.boot.context.config.ConfigDataLocationNotFoundException;
import org.springframework.boot.env.BootstrapRegistry.Registration;
import static org.springframework.cloud.consul.config.ConsulConfigProperties.Format.FILES;
public class ConsulConfigDataLoader
implements ConfigDataLoader<ConsulConfigDataLocation> {
private static final Log log = LogFactory.getLog(ConsulConfigDataLoader.class);
@Override
public ConfigData load(ConfigDataLoaderContext context,
ConsulConfigDataLocation location) {
try {
ConsulClient consul = getBean(context, ConsulClient.class);
ConsulConfigProperties properties = location.getProperties();
ConsulPropertySource propertySource = null;
if (properties.getFormat() == FILES) {
Response<GetValue> response = consul.getKVValue(location.getContext(),
properties.getAclToken());
addIndex(context, location, response.getConsulIndex());
if (response.getValue() != null) {
ConsulFilesPropertySource filesPropertySource = new ConsulFilesPropertySource(
location.getContext(), consul, properties);
filesPropertySource.init(response.getValue());
propertySource = filesPropertySource;
}
else if (!location.isOptional()) {
throw new ConfigDataLocationNotFoundException(location);
}
}
else {
propertySource = create(context, location);
}
return new ConfigData(Collections.singletonList(propertySource));
}
catch (ConfigDataLocationNotFoundException e) {
throw e;
}
catch (Exception e) {
if (location.getProperties().isFailFast() || !location.isOptional()) {
throw new ConfigDataLocationNotFoundException(location, e);
}
else {
log.warn("Unable to load consul config from " + location.getContext(), e);
}
}
return null;
}
protected <T> T getBean(ConfigDataLoaderContext context, Class<T> type) {
Registration<T> registration = context.getBootstrapRegistry()
.getRegistration(type);
if (registration == null) {
return null;
}
return registration.get();
}
protected ConsulPropertySource create(ConfigDataLoaderContext context,
ConsulConfigDataLocation location) {
ConsulPropertySource propertySource = new ConsulPropertySource(
location.getContext(), getBean(context, ConsulClient.class),
location.getProperties());
propertySource.init();
addIndex(context, location, propertySource.getInitialIndex());
return propertySource;
}
private void addIndex(ConfigDataLoaderContext context,
ConsulConfigDataLocation location, Long consulIndex) {
ConsulConfigIndexes indexes = getBean(context, ConsulConfigIndexes.class);
if (indexes != null) { // should never be the case
indexes.getIndexes().put(location.getContext(), consulIndex);
}
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2015-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.consul.config;
import java.util.Objects;
import org.springframework.boot.context.config.ConfigDataLocation;
import org.springframework.core.style.ToStringCreator;
public class ConsulConfigDataLocation extends ConfigDataLocation {
private final ConsulConfigProperties properties;
private final String context;
private final boolean optional;
public ConsulConfigDataLocation(ConsulConfigProperties properties, String context,
boolean optional) {
this.properties = properties;
this.context = context;
this.optional = optional;
}
public ConsulConfigProperties getProperties() {
return this.properties;
}
public String getContext() {
return this.context;
}
public boolean isOptional() {
return this.optional;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ConsulConfigDataLocation that = (ConsulConfigDataLocation) o;
return this.optional == that.optional && this.properties.equals(that.properties)
&& this.context.equals(that.context);
}
@Override
public int hashCode() {
return Objects.hash(this.properties, this.context, this.optional);
}
@Override
public String toString() {
return new ToStringCreator(this).append("context", context)
.append("optional", optional).append("properties", properties).toString();
}
}

View File

@@ -0,0 +1,175 @@
/*
* Copyright 2015-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.consul.config;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import com.ecwid.consul.v1.ConsulClient;
import org.springframework.boot.context.config.ConfigDataLocationNotFoundException;
import org.springframework.boot.context.config.ConfigDataLocationResolver;
import org.springframework.boot.context.config.ConfigDataLocationResolverContext;
import org.springframework.boot.context.config.Profiles;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.cloud.consul.ConsulAutoConfiguration;
import org.springframework.cloud.consul.ConsulProperties;
import org.springframework.util.StringUtils;
import static org.springframework.cloud.consul.config.ConsulConfigProperties.Format.FILES;
public class ConsulConfigDataLocationResolver
implements ConfigDataLocationResolver<ConsulConfigDataLocation> {
@Override
public boolean isResolvable(ConfigDataLocationResolverContext context,
String location) {
boolean enabled = context.getBinder()
.bind(ConsulProperties.PREFIX + ".enabled", Boolean.class).orElse(true);
boolean configEnabled = context.getBinder()
.bind(ConsulConfigProperties.PREFIX + ".enabled", Boolean.class)
.orElse(true);
return location.startsWith("consul:") && configEnabled && enabled;
}
@Override
public List<ConsulConfigDataLocation> resolve(
ConfigDataLocationResolverContext context, String location, boolean optional)
throws ConfigDataLocationNotFoundException {
return Collections.emptyList();
}
@Override
public List<ConsulConfigDataLocation> resolveProfileSpecific(
ConfigDataLocationResolverContext context, String location, boolean optional,
Profiles profiles) throws ConfigDataLocationNotFoundException {
//TODO: add support for consul host and port from location
ConsulConfigProperties properties = loadConfigProperties(context.getBinder());
String appName = properties.getName();
if (StringUtils.isEmpty(appName)) {
appName = context.getBinder().bind("spring.application.name", String.class)
.orElse("application");
}
String prefix = properties.getPrefix();
List<String> suffixes = new ArrayList<>();
if (properties.getFormat() != FILES) {
suffixes.add("/");
}
else {
suffixes.add(".yml");
suffixes.add(".yaml");
suffixes.add(".properties");
}
String defaultContext = getContext(prefix, properties.getDefaultContext());
List<String> contexts = new ArrayList<>();
for (String suffix : suffixes) {
contexts.add(defaultContext + suffix);
}
for (String suffix : suffixes) {
addProfiles(contexts, defaultContext, profiles, suffix, properties);
}
String baseContext = getContext(prefix, appName);
for (String suffix : suffixes) {
contexts.add(baseContext + suffix);
}
for (String suffix : suffixes) {
addProfiles(contexts, baseContext, profiles, suffix, properties);
}
Collections.reverse(contexts);
// TODO use location for host:port
ConsulClient consul = createConsulClient(context);
registerBean(context, ConsulClient.class, consul);
ConsulConfigDataIndexes indexes = new ConsulConfigDataIndexes();
registerBean(context, ConsulConfigIndexes.class, indexes);
ArrayList<ConsulConfigDataLocation> locations = new ArrayList<>();
contexts.forEach(
propertySourceContext -> locations.add(new ConsulConfigDataLocation(
properties, propertySourceContext, optional)));
return locations;
}
protected <T> void registerBean(ConfigDataLocationResolverContext context,
Class<T> type, T instance) {
context.getBootstrapRegistry().register(type, () -> instance)
.onApplicationContextPrepared(
(ctxt, consulClient) -> ctxt.getBeanFactory().registerSingleton(
"configData" + type.getSimpleName(), consulClient));
}
protected ConsulClient createConsulClient(ConfigDataLocationResolverContext context) {
return ConsulAutoConfiguration
.createConsulClient(loadProperties(context.getBinder()));
}
protected String getContext(String prefix, String context) {
if (StringUtils.isEmpty(prefix)) {
return context;
}
else {
return prefix + "/" + context;
}
}
protected void addProfiles(List<String> contexts, String baseContext,
Profiles profiles, String suffix, ConsulConfigProperties properties) {
for (String profile : profiles.getAccepted()) {
contexts.add(
baseContext + properties.getProfileSeparator() + profile + suffix);
}
}
protected ConsulProperties loadProperties(Binder binder) {
return binder.bind(ConsulProperties.PREFIX, Bindable.of(ConsulProperties.class))
.orElse(new ConsulProperties());
}
protected ConsulConfigProperties loadConfigProperties(Binder binder) {
return binder
.bind(ConsulConfigProperties.PREFIX,
Bindable.of(ConsulConfigProperties.class))
.orElse(new ConsulConfigProperties());
}
protected static class ConsulConfigDataIndexes implements ConsulConfigIndexes {
private final LinkedHashMap<String, Long> indexes = new LinkedHashMap<>();
@Override
public LinkedHashMap<String, Long> getIndexes() {
return indexes;
}
}
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2015-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.consul.config;
import java.util.LinkedHashMap;
public interface ConsulConfigIndexes {
LinkedHashMap<String, Long> getIndexes();
}

View File

@@ -25,13 +25,20 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.style.ToStringCreator;
import org.springframework.validation.annotation.Validated;
import static org.springframework.cloud.consul.config.ConsulConfigProperties.PREFIX;
/**
* @author Spencer Gibb
*/
@ConfigurationProperties("spring.cloud.consul.config")
@ConfigurationProperties(PREFIX)
@Validated
public class ConsulConfigProperties {
/**
* Prefix for configuration properties.
*/
public static final String PREFIX = "spring.cloud.consul.config";
private boolean enabled = true;
private String prefix = "config";

View File

@@ -46,7 +46,8 @@ import static org.springframework.cloud.consul.config.ConsulConfigProperties.For
* @author Spencer Gibb
*/
@Order(0)
public class ConsulPropertySourceLocator implements PropertySourceLocator {
public class ConsulPropertySourceLocator
implements PropertySourceLocator, ConsulConfigIndexes {
private static final Log log = LogFactory.getLog(ConsulPropertySourceLocator.class);
@@ -69,7 +70,8 @@ public class ConsulPropertySourceLocator implements PropertySourceLocator {
return this.contexts;
}
public LinkedHashMap<String, Long> getContextIndexes() {
@Override
public LinkedHashMap<String, Long> getIndexes() {
return this.contextIndex;
}
@@ -86,8 +88,7 @@ public class ConsulPropertySourceLocator implements PropertySourceLocator {
ConfigurableEnvironment env = (ConfigurableEnvironment) environment;
String appName = this.properties.getName();
if (appName == null) {
if (StringUtils.isEmpty(appName)) {
appName = env.getProperty("spring.application.name");
}

View File

@@ -4,3 +4,11 @@ org.springframework.cloud.consul.config.ConsulConfigAutoConfiguration
# Bootstrap Configuration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.consul.config.ConsulConfigBootstrapConfiguration
# ConfigData Location Resolvers
org.springframework.boot.context.config.ConfigDataLocationResolver=\
org.springframework.cloud.consul.config.ConsulConfigDataLocationResolver
# ConfigData Loaders
org.springframework.boot.context.config.ConfigDataLoader=\
org.springframework.cloud.consul.config.ConsulConfigDataLoader

View File

@@ -0,0 +1,174 @@
/*
* Copyright 2013-2019 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.consul.config;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import com.ecwid.consul.v1.ConsulClient;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.consul.test.ConsulTestcontainers;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.test.annotation.DirtiesContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Spencer Gibb
*/
@DirtiesContext
public class ConsulConfigDataIntegrationTests {
private static final String APP_NAME = "testConsulConfigData";
private static final String PREFIX = "_configDataIntegrationTests_config__";
private static final String ROOT = PREFIX + UUID.randomUUID();
private static final String VALUE1 = "testPropVal";
private static final String TEST_PROP = "testProp";
private static final String TEST_PROP_CANONICAL = "test-prop";
private static final String KEY1 = ROOT + "/application/" + TEST_PROP;
private static final String VALUE2 = "testPropVal2";
private static final String TEST_PROP2 = "testProp2";
private static final String TEST_PROP2_CANONICAL = "test-prop2";
private static final String KEY2 = ROOT + "/application/" + TEST_PROP2;
private static final String TEST_PROP3 = "testProp3";
private static final String TEST_PROP3_CANONICAL = "test-prop3";
private static final String KEY3 = ROOT + "/" + APP_NAME + "/" + TEST_PROP3;
private static ConfigurableApplicationContext context;
private static ConfigurableEnvironment environment;
private static ConsulClient client;
@BeforeAll
public static void setup() {
ConsulTestcontainers.start();
client = ConsulTestcontainers.client();
client.deleteKVValues(PREFIX);
client.setKVValue(KEY1, VALUE1);
client.setKVValue(KEY2, VALUE2);
context = new SpringApplicationBuilder(Config.class).web(WebApplicationType.NONE)
.run("--spring.application.name=" + APP_NAME,
"--spring.config.import=consul:", "--debug",
"--spring.cloud.consul.host=" + ConsulTestcontainers.getHost(),
"--spring.cloud.consul.port=" + ConsulTestcontainers.getPort(),
"--spring.cloud.consul.config.prefix=" + ROOT,
"--spring.cloud.consul.config.watch.delay=10");
client = context.getBean(ConsulClient.class);
environment = context.getEnvironment();
}
@AfterAll
public static void teardown() {
client.deleteKVValues(PREFIX);
if (context != null) {
context.close();
}
}
@Test
public void propertyLoaded() {
String testProp = environment.getProperty(TEST_PROP2_CANONICAL);
assertThat(testProp).as("testProp was wrong").isEqualTo(VALUE2);
}
@Test
public void propertyLoadedAndUpdated() throws Exception {
String testProp = environment.getProperty(TEST_PROP_CANONICAL);
assertThat(testProp).as("testProp was wrong").isEqualTo(VALUE1);
client.setKVValue(KEY1, "testPropValUpdate");
CountDownLatch latch = context.getBean("countDownLatch1", CountDownLatch.class);
boolean receivedEvent = latch.await(15, TimeUnit.SECONDS);
assertThat(receivedEvent).as("listener didn't receive event").isTrue();
testProp = environment.getProperty(TEST_PROP_CANONICAL);
assertThat(testProp).as("testProp was wrong after update")
.isEqualTo("testPropValUpdate");
}
@Test
public void contextDoesNotExistThenExists() throws Exception {
String testProp = environment.getProperty(TEST_PROP3_CANONICAL);
assertThat(testProp).as("testProp was wrong").isNull();
client.setKVValue(KEY3, "testPropValInsert");
CountDownLatch latch = context.getBean("countDownLatch2", CountDownLatch.class);
boolean receivedEvent = latch.await(15, TimeUnit.SECONDS);
assertThat(receivedEvent).as("listener didn't receive event").isTrue();
testProp = environment.getProperty(TEST_PROP3_CANONICAL);
assertThat(testProp).as(TEST_PROP3 + " was wrong after update")
.isEqualTo("testPropValInsert");
}
@Configuration
@EnableAutoConfiguration
static class Config implements ApplicationListener<EnvironmentChangeEvent> {
@Bean
public CountDownLatch countDownLatch1() {
return new CountDownLatch(1);
}
@Bean
public CountDownLatch countDownLatch2() {
return new CountDownLatch(1);
}
@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
if (event.getKeys().contains(TEST_PROP)) {
countDownLatch1().countDown();
}
else if (event.getKeys().contains(TEST_PROP3)) {
countDownLatch2().countDown();
}
}
}
}

View File

@@ -21,20 +21,19 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import com.ecwid.consul.v1.ConsulClient;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.consul.test.ConsulTestcontainers;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.test.annotation.DirtiesContext;
@@ -74,85 +73,81 @@ public class ConsulPropertySourceLocatorTests {
private static final String KEY3 = ROOT + "/" + APP_NAME + "/" + TEST_PROP3;
private ConfigurableApplicationContext context;
private static ConfigurableApplicationContext context;
private ConfigurableEnvironment environment;
private static ConfigurableEnvironment environment;
private ConsulClient client;
private static ConsulClient client;
@Before
public void setup() {
@BeforeAll
public static void setup() {
ConsulTestcontainers.start();
this.client = ConsulTestcontainers.client();
this.client.deleteKVValues(PREFIX);
this.client.setKVValue(KEY1, VALUE1);
this.client.setKVValue(KEY2, VALUE2);
client = ConsulTestcontainers.client();
client.deleteKVValues(PREFIX);
client.setKVValue(KEY1, VALUE1);
client.setKVValue(KEY2, VALUE2);
this.context = new SpringApplicationBuilder(Config.class)
.web(WebApplicationType.NONE).run("--SPRING_APPLICATION_NAME=" + APP_NAME,
context = new SpringApplicationBuilder(Config.class).web(WebApplicationType.NONE)
.run("--spring.application.name=" + APP_NAME,
"--spring.config.use-legacy-processing=true",
"--spring.cloud.consul.host=" + ConsulTestcontainers.getHost(),
"--spring.cloud.consul.port=" + ConsulTestcontainers.getPort(),
"--spring.cloud.consul.config.prefix=" + ROOT,
"spring.cloud.consul.config.watch.delay=10");
"--spring.cloud.consul.config.watch.delay=10");
this.client = this.context.getBean(ConsulClient.class);
this.environment = this.context.getEnvironment();
client = context.getBean(ConsulClient.class);
environment = context.getEnvironment();
}
@After
public void teardown() {
this.client.deleteKVValues(PREFIX);
@AfterAll
public static void teardown() {
client.deleteKVValues(PREFIX);
if (context != null) {
this.context.close();
context.close();
}
}
@Test
public void propertyLoaded() throws Exception {
String testProp = this.environment.getProperty(TEST_PROP2_CANONICAL);
public void propertyLoaded() {
String testProp = environment.getProperty(TEST_PROP2_CANONICAL);
assertThat(testProp).as("testProp was wrong").isEqualTo(VALUE2);
}
@Test
@Ignore // FIXME: broken tests with boot 2.0.0
public void propertyLoadedAndUpdated() throws Exception {
String testProp = this.environment.getProperty(TEST_PROP_CANONICAL);
String testProp = environment.getProperty(TEST_PROP_CANONICAL);
assertThat(testProp).as("testProp was wrong").isEqualTo(VALUE1);
this.client.setKVValue(KEY1, "testPropValUpdate");
client.setKVValue(KEY1, "testPropValUpdate");
CountDownLatch latch = this.context.getBean("countDownLatch1",
CountDownLatch.class);
CountDownLatch latch = context.getBean("countDownLatch1", CountDownLatch.class);
boolean receivedEvent = latch.await(15, TimeUnit.SECONDS);
assertThat(receivedEvent).as("listener didn't receive event").isTrue();
testProp = this.environment.getProperty(TEST_PROP_CANONICAL);
testProp = environment.getProperty(TEST_PROP_CANONICAL);
assertThat(testProp).as("testProp was wrong after update")
.isEqualTo("testPropValUpdate");
}
@Test
@Ignore // FIXME: broken tests with boot 2.0.0
public void contextDoesNotExistThenExists() throws Exception {
String testProp = this.environment.getProperty(TEST_PROP3_CANONICAL);
String testProp = environment.getProperty(TEST_PROP3_CANONICAL);
assertThat(testProp).as("testProp was wrong").isNull();
this.client.setKVValue(KEY3, "testPropValInsert");
client.setKVValue(KEY3, "testPropValInsert");
CountDownLatch latch = this.context.getBean("countDownLatch2",
CountDownLatch.class);
CountDownLatch latch = context.getBean("countDownLatch2", CountDownLatch.class);
boolean receivedEvent = latch.await(15, TimeUnit.SECONDS);
assertThat(receivedEvent).as("listener didn't receive event").isTrue();
testProp = this.environment.getProperty(TEST_PROP3_CANONICAL);
testProp = environment.getProperty(TEST_PROP3_CANONICAL);
assertThat(testProp).as(TEST_PROP3 + " was wrong after update")
.isEqualTo("testPropValInsert");
}
@Configuration(proxyBeanMethods = false)
@Configuration
@EnableAutoConfiguration
static class Config {
static class Config implements ApplicationListener<EnvironmentChangeEvent> {
@Bean
public CountDownLatch countDownLatch1() {
@@ -164,8 +159,8 @@ public class ConsulPropertySourceLocatorTests {
return new CountDownLatch(1);
}
@EventListener
public void handle(EnvironmentChangeEvent event) {
@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
if (event.getKeys().contains(TEST_PROP)) {
countDownLatch1().countDown();
}

View File

@@ -54,6 +54,10 @@ public class ConsulAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public ConsulClient consulClient(ConsulProperties consulProperties) {
return createConsulClient(consulProperties);
}
public static ConsulClient createConsulClient(ConsulProperties consulProperties) {
final int agentPort = consulProperties.getPort();
final String agentHost = !StringUtils.isEmpty(consulProperties.getScheme())
? consulProperties.getScheme() + "://" + consulProperties.getHost()