Adds support for paths in ConfigData

Now `consul:myhost:8501/mypath1;/mypath2;/mypath3` will use the paths separated by semicolon (;) instead of automatically generating paths. `consul:myhost:8501` or `consul:` will automatically generate paths from ConsulConfigProperties and active profiles.
This commit is contained in:
spencergibb
2020-09-09 22:42:42 -04:00
parent c0cb7717c8
commit d723dcbe31
3 changed files with 213 additions and 58 deletions

View File

@@ -17,10 +17,12 @@
package org.springframework.cloud.consul.config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import com.ecwid.consul.v1.ConsulClient;
@@ -32,13 +34,21 @@ 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.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import static org.springframework.cloud.consul.config.ConsulConfigProperties.Format.FILES;
public class ConsulConfigDataLocationResolver
implements ConfigDataLocationResolver<ConsulConfigDataLocation> {
protected static final List<String> DIR_SUFFIXES = Collections.singletonList("/");
protected static final List<String> FILES_SUFFIXES = Collections
.unmodifiableList(Arrays.asList(".yml", ".yaml", ".properties"));
@Override
public boolean isResolvable(ConfigDataLocationResolverContext context,
String location) {
@@ -62,7 +72,10 @@ public class ConsulConfigDataLocationResolver
ConfigDataLocationResolverContext context, String location, boolean optional,
Profiles profiles) throws ConfigDataLocationNotFoundException {
ConsulConfigProperties properties = loadConfigProperties(context.getBinder());
UriComponents locationUri = parseLocation(context, location);
ConsulConfigProperties properties = loadConfigProperties(context.getBinder(),
locationUri);
String appName = properties.getName();
if (StringUtils.isEmpty(appName)) {
@@ -70,74 +83,68 @@ public class ConsulConfigDataLocationResolver
.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");
List<String> contexts = (CollectionUtils.isEmpty(locationUri.getPathSegments()))
? getAutomaticContexts(profiles, properties, appName)
: getCustomContexts(locationUri, properties);
registerBean(context, ConsulClient.class,
() -> createConsulClient(context, locationUri));
registerBean(context, ConsulConfigIndexes.class, ConsulConfigDataIndexes::new);
return contexts.stream()
.map(propertySourceContext -> new ConsulConfigDataLocation(properties,
propertySourceContext, optional))
.collect(Collectors.toList());
}
private List<String> getCustomContexts(UriComponents uriComponents,
ConsulConfigProperties properties) {
List<String> contexts = new ArrayList<>();
if (StringUtils.isEmpty(uriComponents.getPath())) {
return contexts;
}
String defaultContext = getContext(prefix, properties.getDefaultContext());
for (String path : uriComponents.getPath().split(";")) {
for (String suffix : getSuffixes(properties)) {
contexts.add(path + suffix);
}
}
return contexts;
}
protected List<String> getSuffixes(ConsulConfigProperties properties) {
if (properties.getFormat() == FILES) {
return FILES_SUFFIXES;
}
return DIR_SUFFIXES;
}
protected List<String> getAutomaticContexts(Profiles profiles,
ConsulConfigProperties properties, String appName) {
List<String> contexts = new ArrayList<>();
for (String suffix : suffixes) {
String prefix = properties.getPrefix();
String defaultContext = getContext(prefix, properties.getDefaultContext());
for (String suffix : getSuffixes(properties)) {
contexts.add(defaultContext + suffix);
}
for (String suffix : suffixes) {
for (String suffix : getSuffixes(properties)) {
addProfiles(contexts, defaultContext, profiles, suffix, properties);
}
String baseContext = getContext(prefix, appName);
for (String suffix : suffixes) {
for (String suffix : getSuffixes(properties)) {
contexts.add(baseContext + suffix);
}
for (String suffix : suffixes) {
for (String suffix : getSuffixes(properties)) {
addProfiles(contexts, baseContext, profiles, suffix, properties);
}
// we build them backwards, first wins, so reverse
Collections.reverse(contexts);
registerBean(context, ConsulClient.class, () -> createConsulClient(context, location));
registerBean(context, ConsulConfigIndexes.class, ConsulConfigDataIndexes::new);
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, Supplier<T> supplier) {
context.getBootstrapRegistry().register(type, supplier)
.onApplicationContextPrepared(
(ctxt, consulClient) -> ctxt.getBeanFactory().registerSingleton(
"configData" + type.getSimpleName(), consulClient));
}
protected ConsulClient createConsulClient(ConfigDataLocationResolverContext context, String location) {
ConsulProperties properties = loadProperties(context.getBinder());
String hostPort = location.substring("consul:".length());
if (StringUtils.hasText(hostPort)) {
String[] split = hostPort.split(":");
if (split.length == 2) { // host and port
properties.setHost(split[0]);
properties.setPort(Integer.parseInt(split[1]));
}
}
//TODO: add support for ConsulConfigProperties.prefix
return ConsulAutoConfiguration.createConsulClient(properties);
return contexts;
}
protected String getContext(String prefix, String context) {
@@ -157,12 +164,52 @@ public class ConsulConfigDataLocationResolver
}
}
protected ConsulProperties loadProperties(Binder binder) {
return binder.bind(ConsulProperties.PREFIX, Bindable.of(ConsulProperties.class))
.orElse(new ConsulProperties());
protected UriComponents parseLocation(ConfigDataLocationResolverContext context,
String location) {
String uri = location.substring("consul:".length());
if (!StringUtils.hasText(uri)) {
return null;
}
if (!uri.startsWith("//")) {
uri = "consul:" + "//" + uri;
}
else {
uri = location;
}
return UriComponentsBuilder.fromUriString(uri).build();
}
protected ConsulConfigProperties loadConfigProperties(Binder binder) {
protected <T> void registerBean(ConfigDataLocationResolverContext context,
Class<T> type, Supplier<T> supplier) {
context.getBootstrapRegistry().register(type, supplier)
.onApplicationContextPrepared(
(ctxt, consulClient) -> ctxt.getBeanFactory().registerSingleton(
"configData" + type.getSimpleName(), consulClient));
}
protected ConsulClient createConsulClient(ConfigDataLocationResolverContext context,
UriComponents location) {
ConsulProperties properties = loadProperties(context.getBinder(), location);
return ConsulAutoConfiguration.createConsulClient(properties);
}
protected ConsulProperties loadProperties(Binder binder, UriComponents location) {
ConsulProperties consulProperties = binder
.bind(ConsulProperties.PREFIX, Bindable.of(ConsulProperties.class))
.orElse(new ConsulProperties());
if (StringUtils.hasText(location.getHost())) {
consulProperties.setHost(location.getHost());
}
if (location.getPort() >= 0) {
consulProperties.setPort(location.getPort());
}
return consulProperties;
}
protected ConsulConfigProperties loadConfigProperties(Binder binder,
UriComponents location) {
return binder
.bind(ConsulConfigProperties.PREFIX,
Bindable.of(ConsulConfigProperties.class))

View File

@@ -89,8 +89,8 @@ public class ConsulConfigDataIntegrationTests {
context = new SpringApplicationBuilder(Config.class).web(WebApplicationType.NONE)
.run("--spring.application.name=" + APP_NAME,
"--spring.config.import=consul:" + ConsulTestcontainers.getHost() + ":" +
ConsulTestcontainers.getPort(),
"--spring.config.import=consul:" + ConsulTestcontainers.getHost()
+ ":" + ConsulTestcontainers.getPort(),
"--spring.cloud.consul.config.prefix=" + ROOT,
"--spring.cloud.consul.config.watch.delay=10");

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 java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.config.ConfigDataLocationResolverContext;
import org.springframework.boot.context.config.Profiles;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.cloud.consul.ConsulProperties;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ConsulConfigDataLocationResolverTests {
@Test
public void testParseLocation() {
ConsulConfigDataLocationResolver resolver = new ConsulConfigDataLocationResolver();
UriComponents uriComponents = resolver.parseLocation(null,
"consul:myhost:8501/mypath1;/mypath2;/mypath3");
assertThat(uriComponents.toUri()).hasScheme("consul").hasHost("myhost")
.hasPort(8501).hasPath("/mypath1;/mypath2;/mypath3");
uriComponents = resolver.parseLocation(null, "consul:myhost:8501");
assertThat(uriComponents.toUri()).hasScheme("consul").hasHost("myhost")
.hasPort(8501).hasPath("");
}
@Test
public void testResolveProfileSpecificWithCustomPaths() {
String location = "consul:myhost:8501/mypath1;/mypath2;/mypath3";
List<ConsulConfigDataLocation> locations = testResolveProfileSpecific(location);
assertThat(locations).hasSize(3);
assertThat(toContexts(locations)).containsExactly("/mypath1/", "/mypath2/",
"/mypath3/");
}
@Test
public void testResolveProfileSpecificWithAutomaticPaths() {
String location = "consul:myhost";
List<ConsulConfigDataLocation> locations = testResolveProfileSpecific(location);
assertThat(locations).hasSize(4);
assertThat(toContexts(locations)).containsExactly("config/testapp,dev/",
"config/testapp/", "config/application,dev/", "config/application/");
}
@Test
public void testLoadProperties() {
ConsulProperties properties = createResolver().loadProperties(
Binder.get(new MockEnvironment()),
UriComponentsBuilder.fromUriString("consul://myhost:8502").build());
assertThat(properties.getHost()).isEqualTo("myhost");
assertThat(properties.getPort()).isEqualTo(8502);
}
private List<String> toContexts(List<ConsulConfigDataLocation> locations) {
return locations.stream().map(ConsulConfigDataLocation::getContext)
.collect(Collectors.toList());
}
private List<ConsulConfigDataLocation> testResolveProfileSpecific(String location) {
ConsulConfigDataLocationResolver resolver = createResolver();
ConfigDataLocationResolverContext context = mock(
ConfigDataLocationResolverContext.class);
MockEnvironment env = new MockEnvironment();
env.setProperty("spring.application.name", "testapp");
when(context.getBinder()).thenReturn(Binder.get(env));
Profiles profiles = mock(Profiles.class);
when(profiles.getAccepted()).thenReturn(Collections.singletonList("dev"));
return resolver.resolveProfileSpecific(context, location, false, profiles);
}
private ConsulConfigDataLocationResolver createResolver() {
ConsulConfigDataLocationResolver resolver = new ConsulConfigDataLocationResolver() {
@Override
protected <T> void registerBean(ConfigDataLocationResolverContext context,
Class<T> type, Supplier<T> supplier) {
// do nothing
}
};
return resolver;
}
}