From 989485a4e5f3077ae04f3ed9f99474a2ac21c072 Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Wed, 9 Sep 2020 16:16:52 -0400 Subject: [PATCH] Adds support for spring.config.import=consul: (#672) --- .../cloud/consul/config/ConfigWatch.java | 123 ++++++------ .../config/ConsulConfigAutoConfiguration.java | 21 ++- .../consul/config/ConsulConfigDataLoader.java | 108 +++++++++++ .../config/ConsulConfigDataLocation.java | 76 ++++++++ .../ConsulConfigDataLocationResolver.java | 175 ++++++++++++++++++ .../consul/config/ConsulConfigIndexes.java | 25 +++ .../consul/config/ConsulConfigProperties.java | 9 +- .../config/ConsulPropertySourceLocator.java | 9 +- .../main/resources/META-INF/spring.factories | 8 + .../ConsulConfigDataIntegrationTests.java | 174 +++++++++++++++++ .../ConsulPropertySourceLocatorTests.java | 77 ++++---- .../cloud/consul/ConsulAutoConfiguration.java | 4 + 12 files changed, 697 insertions(+), 112 deletions(-) create mode 100644 spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigDataLoader.java create mode 100644 spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigDataLocation.java create mode 100644 spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigDataLocationResolver.java create mode 100644 spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigIndexes.java create mode 100644 spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigDataIntegrationTests.java diff --git a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConfigWatch.java b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConfigWatch.java index fef767c7..1ec886e6 100644 --- a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConfigWatch.java +++ b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConfigWatch.java @@ -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> response = this.consul.getKVValues(context, - aclToken, - new QueryParams(this.properties.getWatch().getWaitTime(), - currentIndex)); + Response> 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()); } } } diff --git a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigAutoConfiguration.java b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigAutoConfiguration.java index 1997c277..f9ebf899 100644 --- a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigAutoConfiguration.java +++ b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigAutoConfiguration.java @@ -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(); } diff --git a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigDataLoader.java b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigDataLoader.java new file mode 100644 index 00000000..e181e24d --- /dev/null +++ b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigDataLoader.java @@ -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 { + + 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 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 getBean(ConfigDataLoaderContext context, Class type) { + Registration 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); + } + } + +} diff --git a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigDataLocation.java b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigDataLocation.java new file mode 100644 index 00000000..4988019d --- /dev/null +++ b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigDataLocation.java @@ -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(); + + } + +} diff --git a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigDataLocationResolver.java b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigDataLocationResolver.java new file mode 100644 index 00000000..a8b597cb --- /dev/null +++ b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigDataLocationResolver.java @@ -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 { + + @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 resolve( + ConfigDataLocationResolverContext context, String location, boolean optional) + throws ConfigDataLocationNotFoundException { + return Collections.emptyList(); + } + + @Override + public List 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 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 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 locations = new ArrayList<>(); + contexts.forEach( + propertySourceContext -> locations.add(new ConsulConfigDataLocation( + properties, propertySourceContext, optional))); + + return locations; + } + + protected void registerBean(ConfigDataLocationResolverContext context, + Class 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 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 indexes = new LinkedHashMap<>(); + + @Override + public LinkedHashMap getIndexes() { + return indexes; + } + + } + +} diff --git a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigIndexes.java b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigIndexes.java new file mode 100644 index 00000000..aae3efd1 --- /dev/null +++ b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigIndexes.java @@ -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 getIndexes(); + +} diff --git a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigProperties.java b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigProperties.java index 159ab57e..8046abfb 100644 --- a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigProperties.java +++ b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigProperties.java @@ -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"; diff --git a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulPropertySourceLocator.java b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulPropertySourceLocator.java index 0b92beaa..b03822f9 100644 --- a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulPropertySourceLocator.java +++ b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulPropertySourceLocator.java @@ -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 getContextIndexes() { + @Override + public LinkedHashMap 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"); } diff --git a/spring-cloud-consul-config/src/main/resources/META-INF/spring.factories b/spring-cloud-consul-config/src/main/resources/META-INF/spring.factories index 1e7e41f9..fc259785 100644 --- a/spring-cloud-consul-config/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-consul-config/src/main/resources/META-INF/spring.factories @@ -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 diff --git a/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigDataIntegrationTests.java b/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigDataIntegrationTests.java new file mode 100644 index 00000000..541d31dc --- /dev/null +++ b/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigDataIntegrationTests.java @@ -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 { + + @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(); + } + } + + } + +} diff --git a/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulPropertySourceLocatorTests.java b/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulPropertySourceLocatorTests.java index 2ad9ef0b..21664a59 100644 --- a/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulPropertySourceLocatorTests.java +++ b/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulPropertySourceLocatorTests.java @@ -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 { @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(); } diff --git a/spring-cloud-consul-core/src/main/java/org/springframework/cloud/consul/ConsulAutoConfiguration.java b/spring-cloud-consul-core/src/main/java/org/springframework/cloud/consul/ConsulAutoConfiguration.java index b2cb0ceb..3ae5d638 100644 --- a/spring-cloud-consul-core/src/main/java/org/springframework/cloud/consul/ConsulAutoConfiguration.java +++ b/spring-cloud-consul-core/src/main/java/org/springframework/cloud/consul/ConsulAutoConfiguration.java @@ -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()