From 3c4ceb0823ce467c2c8ebc27b1c82c3d14d56331 Mon Sep 17 00:00:00 2001 From: Colin Mullikin Date: Tue, 26 Nov 2019 17:48:17 -0600 Subject: [PATCH] Add support for multiple configuration path prefixes. Fixes gh-450 --- .../main/asciidoc/spring-cloud-consul.adoc | 2 +- .../consul/config/ConsulConfigProperties.java | 35 +++++- .../config/ConsulPropertySourceLocator.java | 4 +- .../consul/config/ConsulPropertySources.java | 33 ++--- ...nsulConfigBootstrapConfigurationTests.java | 8 +- ...nfigDataCustomizationIntegrationTests.java | 2 +- ...gDataMultiplePrefixesIntegrationTests.java | 119 ++++++++++++++++++ .../ConsulPropertySourceLocatorTests.java | 2 +- 8 files changed, 176 insertions(+), 29 deletions(-) create mode 100644 spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigDataMultiplePrefixesIntegrationTests.java diff --git a/docs/src/main/asciidoc/spring-cloud-consul.adoc b/docs/src/main/asciidoc/spring-cloud-consul.adoc index 1c59a9a7..e6d2f58a 100644 --- a/docs/src/main/asciidoc/spring-cloud-consul.adoc +++ b/docs/src/main/asciidoc/spring-cloud-consul.adoc @@ -339,7 +339,7 @@ The watch uses a Spring `TaskScheduler` to schedule the call to consul. By defau [[spring-cloud-consul-config]] == Distributed Configuration with Consul -Consul provides a https://consul.io/docs/agent/http/kv.html[Key/Value Store] for storing configuration and other metadata. Spring Cloud Consul Config is an alternative to the https://github.com/spring-cloud/spring-cloud-config[Config Server and Client]. Configuration is loaded into the Spring Environment during the special "bootstrap" phase. Configuration is stored in the `/config` folder by default. Multiple `PropertySource` instances are created based on the application's name and the active profiles that mimicks the Spring Cloud Config order of resolving properties. For example, an application with the name "testApp" and with the "dev" profile will have the following property sources created: +Consul provides a https://consul.io/docs/agent/http/kv.html[Key/Value Store] for storing configuration and other metadata. Spring Cloud Consul Config is an alternative to the https://github.com/spring-cloud/spring-cloud-config[Config Server and Client]. Configuration is loaded into the Spring Environment during the special "bootstrap" phase. Configuration is stored in the `/config` folder by default. Multiple `PropertySource` instances are created based on the application's name and the active profiles that mimics the Spring Cloud Config order of resolving properties. For example, an application with the name "testApp" and with the "dev" profile will have the following property sources created: ---- config/testApp,dev/ 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 a2413b30..87513b92 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 @@ -16,13 +16,19 @@ package org.springframework.cloud.consul.config; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + import javax.annotation.PostConstruct; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.core.style.ToStringCreator; +import org.springframework.util.CollectionUtils; import org.springframework.validation.annotation.Validated; import static org.springframework.cloud.consul.config.ConsulConfigProperties.PREFIX; @@ -41,7 +47,7 @@ public class ConsulConfigProperties { private boolean enabled = true; - private String prefix = "config"; + private List prefixes = new ArrayList<>(Collections.singletonList("config")); @NotEmpty private String defaultContext = "application"; @@ -92,12 +98,31 @@ public class ConsulConfigProperties { this.enabled = enabled; } - public String getPrefix() { - return this.prefix; + public List getPrefixes() { + return this.prefixes; } + public void setPrefixes(List prefixes) { + this.prefixes = prefixes; + } + + @DeprecatedConfigurationProperty(reason = "replaced to support multiple prefixes", + replacement = PREFIX + ".prefixes") + public String getPrefix() { + if (CollectionUtils.isEmpty(this.prefixes)) { + return null; + } + return this.prefixes.get(0); + } + + @Deprecated public void setPrefix(String prefix) { - this.prefix = prefix; + if (prefix != null) { + this.prefixes = new ArrayList<>(Collections.singletonList(prefix)); + } + else { + this.prefixes = new ArrayList<>(); + } } public @NotEmpty String getDefaultContext() { @@ -166,7 +191,7 @@ public class ConsulConfigProperties { @Override public String toString() { - return new ToStringCreator(this).append("enabled", this.enabled).append("prefix", this.prefix) + return new ToStringCreator(this).append("enabled", this.enabled).append("prefixes", this.prefixes) .append("defaultContext", this.defaultContext).append("profileSeparator", this.profileSeparator) .append("format", this.format).append("dataKey", this.dataKey).append("aclToken", this.aclToken) .append("watch", this.watch).append("failFast", this.failFast).append("name", this.name).toString(); 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 f8ef97d6..ee572fb4 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 @@ -85,8 +85,8 @@ public class ConsulPropertySourceLocator implements PropertySourceLocator, Consu CompositePropertySource composite = new CompositePropertySource("consul"); for (String propertySourceContext : this.contexts) { - ConsulPropertySource propertySource = sources.createPropertySource(propertySourceContext, true, - this.consul, contextIndex::put); + ConsulPropertySource propertySource = sources.createPropertySource(propertySourceContext, this.consul, + contextIndex::put); if (propertySource != null) { composite.addPropertySource(propertySource); } diff --git a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulPropertySources.java b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulPropertySources.java index fe8b3505..62fd459b 100644 --- a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulPropertySources.java +++ b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulPropertySources.java @@ -53,24 +53,25 @@ public class ConsulPropertySources { public List getAutomaticContexts(List profiles, boolean reverse) { List contexts = new ArrayList<>(); - String prefix = properties.getPrefix(); - String defaultContext = getContext(prefix, properties.getDefaultContext()); - List suffixes = getSuffixes(); - for (String suffix : suffixes) { - contexts.add(defaultContext + suffix); - } - for (String suffix : suffixes) { - addProfiles(contexts, defaultContext, profiles, suffix); - } + for (String prefix : this.properties.getPrefixes()) { + String defaultContext = getContext(prefix, properties.getDefaultContext()); + List suffixes = getSuffixes(); + for (String suffix : suffixes) { + contexts.add(defaultContext + suffix); + } + for (String suffix : suffixes) { + addProfiles(contexts, defaultContext, profiles, suffix); + } - // getName() defaults to ${spring.application.name} or application - String baseContext = getContext(prefix, properties.getName()); + // getName() defaults to ${spring.application.name} or application + String baseContext = getContext(prefix, properties.getName()); - for (String suffix : suffixes) { - contexts.add(baseContext + suffix); - } - for (String suffix : suffixes) { - addProfiles(contexts, baseContext, profiles, suffix); + for (String suffix : suffixes) { + contexts.add(baseContext + suffix); + } + for (String suffix : suffixes) { + addProfiles(contexts, baseContext, profiles, suffix); + } } if (reverse) { // we build them backwards, first wins, so reverse diff --git a/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigBootstrapConfigurationTests.java b/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigBootstrapConfigurationTests.java index fa666a3e..c6fa1c76 100644 --- a/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigBootstrapConfigurationTests.java +++ b/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigBootstrapConfigurationTests.java @@ -16,6 +16,8 @@ package org.springframework.cloud.consul.config; +import java.util.Collections; + import org.junit.Test; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -39,7 +41,7 @@ public class ConsulConfigBootstrapConfigurationTests { this.contextRunner.withUserConfiguration(TestConfig.class).withInitializer(new ConsulTestcontainers()) .withUserConfiguration(ConsulConfigBootstrapConfiguration.class).run(context -> { ConsulConfigProperties config = context.getBean(ConsulConfigProperties.class); - assertThat(config.getPrefix()).as("Prefix did not match").isEqualTo("platform-config"); + assertThat(config.getPrefixes().get(0)).as("Prefix did not match").isEqualTo("platform-config"); assertThat(config.getDefaultContext()).as("Default context did not match").isEqualTo("defaults"); }); } @@ -53,7 +55,7 @@ public class ConsulConfigBootstrapConfigurationTests { this.contextRunner.withUserConfiguration(ConsulConfigBootstrapConfiguration.class) .withInitializer(new ConsulTestcontainers()).run(context -> { ConsulConfigProperties config = context.getBean(ConsulConfigProperties.class); - assertThat(config.getPrefix()).as("Prefix did not match").isEqualTo("config"); + assertThat(config.getPrefixes().get(0)).as("Prefix did not match").isEqualTo("config"); assertThat(config.getDefaultContext()).as("Default context did not match").isEqualTo("application"); }); } @@ -66,7 +68,7 @@ public class ConsulConfigBootstrapConfigurationTests { @Bean public ConsulConfigProperties consulConfigProperties() { ConsulConfigProperties config = new ConsulConfigProperties(); - config.setPrefix("platform-config"); + config.setPrefixes(Collections.singletonList("platform-config")); config.setDefaultContext("defaults"); return config; } diff --git a/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigDataCustomizationIntegrationTests.java b/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigDataCustomizationIntegrationTests.java index ec54a75f..eff46fb2 100644 --- a/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigDataCustomizationIntegrationTests.java +++ b/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigDataCustomizationIntegrationTests.java @@ -68,7 +68,7 @@ public class ConsulConfigDataCustomizationIntegrationTests { context = application.run("--spring.application.name=" + APP_NAME, "--spring.config.import=consul:" + ConsulTestcontainers.getHost() + ":" + ConsulTestcontainers.getPort(), - "--spring.cloud.consul.config.prefix=" + ROOT, "--spring.cloud.consul.config.watch.delay=10"); + "--spring.cloud.consul.config.prefixes=" + ROOT, "--spring.cloud.consul.config.watch.delay=10"); } diff --git a/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigDataMultiplePrefixesIntegrationTests.java b/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigDataMultiplePrefixesIntegrationTests.java new file mode 100644 index 00000000..0757c580 --- /dev/null +++ b/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConsulConfigDataMultiplePrefixesIntegrationTests.java @@ -0,0 +1,119 @@ +/* + * 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 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.context.ConfigurableApplicationContext; +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 ConsulConfigDataMultiplePrefixesIntegrationTests { + + private static final String APP_NAME = "testConsulConfigData"; + + private static final String PREFIX = "_configDataMultiplePrefixesIntegrationTests_config__"; + + private static final String PREFIX2 = "_configDataMultiplePrefixesIntegrationTests_config2__"; + + private static final String ROOT = PREFIX + UUID.randomUUID(); + + private static final String ROOT2 = PREFIX2 + 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 = ROOT2 + "/application/" + TEST_PROP2; + + 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.deleteKVValues(PREFIX2); + client.setKVValue(KEY1, VALUE1); + client.setKVValue(KEY2, VALUE2); + + context = new SpringApplicationBuilder(Config.class).web(WebApplicationType.NONE).run( + "--logging.level.org.springframework.cloud.consul.config.ConfigWatch=TRACE", + "--spring.application.name=" + APP_NAME, + "--spring.config.import=consul:" + ConsulTestcontainers.getHost() + ":" + + ConsulTestcontainers.getPort(), + "--spring.cloud.consul.config.prefixes=" + ROOT + "," + ROOT2, + "--spring.cloud.consul.config.watch.delay=10", "--spring.cloud.consul.config.watch.wait-time=1"); + + client = context.getBean(ConsulClient.class); + environment = context.getEnvironment(); + } + + @AfterAll + public static void teardown() { + client.deleteKVValues(PREFIX); + client.deleteKVValues(PREFIX2); + if (context != null) { + context.close(); + } + } + + @Test + public void propertyLoaded() { + String testProp = environment.getProperty(TEST_PROP_CANONICAL); + assertThat(testProp).as(TEST_PROP + " was wrong").isEqualTo(VALUE1); + String testProp2 = environment.getProperty(TEST_PROP2_CANONICAL); + assertThat(testProp2).as(TEST_PROP2 + " was wrong").isEqualTo(VALUE2); + } + + @Configuration + @EnableAutoConfiguration + static class Config { + + } + +} 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 778446f3..a75f8adc 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 @@ -91,7 +91,7 @@ public class ConsulPropertySourceLocatorTests { "--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.prefixes=" + ROOT, "--spring.cloud.consul.config.watch.delay=10"); client = context.getBean(ConsulClient.class); environment = context.getEnvironment();