Fixes ordering with local and remote sources with profiles.

If the context is associated with a profile, the Option.PROFILE_SPECIFIC is added.

Fixes gh-706
This commit is contained in:
spencergibb
2021-05-05 20:08:24 -04:00
parent 77416811bf
commit d6ab8a64b6
11 changed files with 237 additions and 127 deletions

View File

@@ -16,21 +16,29 @@
package org.springframework.cloud.consul.config;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import com.ecwid.consul.v1.ConsulClient;
import org.apache.commons.logging.Log;
import org.springframework.boot.context.config.ConfigData;
import org.springframework.boot.context.config.ConfigData.Option;
import org.springframework.boot.context.config.ConfigData.Options;
import org.springframework.boot.context.config.ConfigDataLoader;
import org.springframework.boot.context.config.ConfigDataLoaderContext;
import org.springframework.boot.context.config.ConfigDataResourceNotFoundException;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.cloud.consul.config.ConsulBootstrapper.LoadContext;
import org.springframework.cloud.consul.config.ConsulBootstrapper.LoaderInterceptor;
import org.springframework.util.StringUtils;
public class ConsulConfigDataLoader implements ConfigDataLoader<ConsulConfigDataResource> {
private static final EnumSet<Option> ALL_OPTIONS = EnumSet.allOf(Option.class);
private final Log log;
public ConsulConfigDataLoader(Log log) {
@@ -59,7 +67,27 @@ public class ConsulConfigDataLoader implements ConfigDataLoader<ConsulConfigData
if (propertySource == null) {
return null;
}
return new ConfigData(Collections.singletonList(propertySource));
List<ConsulPropertySource> propertySources = Collections.singletonList(propertySource);
if (ALL_OPTIONS.size() == 1) {
// boot 2.4.2 and prior
return new ConfigData(propertySources);
}
else if (ALL_OPTIONS.size() == 2) {
// boot 2.4.3 and 2.4.4
return new ConfigData(propertySources, Option.IGNORE_IMPORTS, Option.IGNORE_PROFILES);
}
else if (ALL_OPTIONS.size() > 2) {
// boot 2.4.5+
return new ConfigData(propertySources, source -> {
List<Option> options = new ArrayList<>();
options.add(Option.IGNORE_IMPORTS);
options.add(Option.IGNORE_PROFILES);
if (StringUtils.hasText(resource.getProfile())) {
options.add(Option.PROFILE_SPECIFIC);
}
return Options.of(options.toArray(new Option[0]));
});
}
}
catch (Exception e) {
if (log.isDebugEnabled()) {
@@ -67,6 +95,7 @@ public class ConsulConfigDataLoader implements ConfigDataLoader<ConsulConfigData
}
throw new ConfigDataResourceNotFoundException(resource, e);
}
return null;
}
protected <T> T getBean(ConfigDataLoaderContext context, Class<T> type) {

View File

@@ -39,6 +39,7 @@ 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.cloud.consul.config.ConsulPropertySources.Context;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
@@ -99,8 +100,8 @@ public class ConsulConfigDataLocationResolver implements ConfigDataLocationResol
ConsulPropertySources consulPropertySources = new ConsulPropertySources(properties, log);
List<String> contexts = (locationUri == null || CollectionUtils.isEmpty(locationUri.getPathSegments()))
? consulPropertySources.getAutomaticContexts(profiles.getAccepted(), false)
List<Context> contexts = (locationUri == null || CollectionUtils.isEmpty(locationUri.getPathSegments()))
? consulPropertySources.generateAutomaticContexts(profiles.getAccepted(), false)
: getCustomContexts(locationUri, properties);
registerAndPromoteBean(resolverContext, ConsulConfigProperties.class, InstanceSupplier.of(properties));
@@ -108,23 +109,25 @@ public class ConsulConfigDataLocationResolver implements ConfigDataLocationResol
registerAndPromoteBean(resolverContext, ConsulConfigIndexes.class,
InstanceSupplier.from(ConsulConfigDataIndexes::new));
return contexts.stream().map(propertySourceContext -> new ConsulConfigDataResource(propertySourceContext,
properties, consulPropertySources)).collect(Collectors.toList());
return contexts
.stream().map(propertySourceContext -> new ConsulConfigDataResource(propertySourceContext.getPath(),
properties, consulPropertySources, propertySourceContext.getProfile()))
.collect(Collectors.toList());
}
private BindHandler getBindHandler(ConfigDataLocationResolverContext context) {
return context.getBootstrapContext().getOrElse(BindHandler.class, null);
}
private List<String> getCustomContexts(UriComponents uriComponents, ConsulConfigProperties properties) {
private List<Context> getCustomContexts(UriComponents uriComponents, ConsulConfigProperties properties) {
if (!StringUtils.hasText(uriComponents.getPath())) {
return Collections.emptyList();
}
List<String> contexts = new ArrayList<>();
List<Context> contexts = new ArrayList<>();
for (String path : uriComponents.getPath().split(";")) {
for (String suffix : getSuffixes(properties)) {
contexts.add(path + suffix);
contexts.add(new Context(path + suffix));
}
}

View File

@@ -31,6 +31,18 @@ public class ConsulConfigDataResource extends ConfigDataResource {
private final ConsulPropertySources consulPropertySources;
private final String profile;
public ConsulConfigDataResource(String context, ConsulConfigProperties properties,
ConsulPropertySources consulPropertySources, String profile) {
this.properties = properties;
this.context = context;
this.optional = true;
this.consulPropertySources = consulPropertySources;
this.profile = profile;
}
@Deprecated
public ConsulConfigDataResource(String context, ConsulConfigProperties properties,
ConsulPropertySources consulPropertySources) {
this(context, true, properties, consulPropertySources);
@@ -43,6 +55,7 @@ public class ConsulConfigDataResource extends ConfigDataResource {
this.context = context;
this.optional = optional;
this.consulPropertySources = consulPropertySources;
this.profile = null;
}
public String getContext() {
@@ -62,6 +75,10 @@ public class ConsulConfigDataResource extends ConfigDataResource {
return this.consulPropertySources;
}
String getProfile() {
return this.profile;
}
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -71,18 +88,19 @@ public class ConsulConfigDataResource extends ConfigDataResource {
return false;
}
ConsulConfigDataResource that = (ConsulConfigDataResource) o;
return this.optional == that.optional && this.context.equals(that.context);
return this.optional == that.optional && this.context.equals(that.context)
&& Objects.equals(this.profile, that.profile);
}
@Override
public int hashCode() {
return Objects.hash(this.context, this.optional);
return Objects.hash(this.context, this.optional, this.profile);
}
@Override
public String toString() {
return new ToStringCreator(this).append("context", context).append("optional", optional)
.append("properties", properties).toString();
.append("properties", properties).append("profile", profile).toString();
}

View File

@@ -21,12 +21,14 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
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.springframework.core.style.ToStringCreator;
import org.springframework.util.StringUtils;
import static org.springframework.cloud.consul.config.ConsulConfigProperties.Format.FILES;
@@ -52,12 +54,16 @@ public class ConsulPropertySources {
}
public List<String> getAutomaticContexts(List<String> profiles, boolean reverse) {
List<String> contexts = new ArrayList<>();
return generateAutomaticContexts(profiles, reverse).stream().map(Context::getPath).collect(Collectors.toList());
}
public List<Context> generateAutomaticContexts(List<String> profiles, boolean reverse) {
List<Context> contexts = new ArrayList<>();
for (String prefix : this.properties.getPrefixes()) {
String defaultContext = getContext(prefix, properties.getDefaultContext());
List<String> suffixes = getSuffixes();
for (String suffix : suffixes) {
contexts.add(defaultContext + suffix);
contexts.add(new Context(defaultContext + suffix));
}
for (String suffix : suffixes) {
addProfiles(contexts, defaultContext, profiles, suffix);
@@ -67,7 +73,7 @@ public class ConsulPropertySources {
String baseContext = getContext(prefix, properties.getName());
for (String suffix : suffixes) {
contexts.add(baseContext + suffix);
contexts.add(new Context(baseContext + suffix));
}
for (String suffix : suffixes) {
addProfiles(contexts, baseContext, profiles, suffix);
@@ -96,9 +102,10 @@ public class ConsulPropertySources {
return DIR_SUFFIXES;
}
private void addProfiles(List<String> contexts, String baseContext, List<String> profiles, String suffix) {
private void addProfiles(List<Context> contexts, String baseContext, List<String> profiles, String suffix) {
for (String profile : profiles) {
contexts.add(baseContext + properties.getProfileSeparator() + profile + suffix);
String path = baseContext + properties.getProfileSeparator() + profile + suffix;
contexts.add(new Context(path, profile));
}
}
@@ -150,6 +157,38 @@ public class ConsulPropertySources {
return propertySource;
}
public static class Context {
private final String path;
private final String profile;
public Context(String path) {
this.path = path;
this.profile = null;
}
public Context(String path, String profile) {
this.path = path;
this.profile = profile;
}
public String getPath() {
return this.path;
}
public String getProfile() {
return this.profile;
}
@Override
public String toString() {
return new ToStringCreator(this).append("path", path).append("profile", profile).toString();
}
}
static class PropertySourceNotFoundException extends RuntimeException {
private final String context;

View File

@@ -28,6 +28,7 @@ import org.springframework.boot.Bootstrapper;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.config.ConfigData;
import org.springframework.boot.context.properties.bind.BindContext;
import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.Bindable;
@@ -36,7 +37,9 @@ import org.springframework.cloud.consul.ConsulProperties;
import org.springframework.cloud.consul.test.ConsulTestcontainers;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.PropertySource;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -65,6 +68,23 @@ public class ConsulConfigDataCustomizationIntegrationTests {
bindHandlerBootstrapper = new BindHandlerBootstrapper();
application.addBootstrapper(bindHandlerBootstrapper);
application.addBootstrapper(ConsulBootstrapper.fromConsulProperties(TestConsulClient::new));
application.addBootstrapper(
registry -> registry.register(ConsulBootstrapper.LoaderInterceptor.class, context1 -> loadContext -> {
ConfigData configData = loadContext.getInvocation().apply(loadContext.getLoaderContext(),
loadContext.getResource());
assertThat(configData).as("ConfigData was null for location %s", loadContext.getResource())
.isNotNull();
assertThat(configData.getPropertySources()).hasSize(1);
PropertySource<?> propertySource = configData.getPropertySources().iterator().next();
ConfigData.Options options = configData.getOptions(propertySource);
assertThat(options).as("ConfigData.options was null for location %s property source %s",
loadContext.getResource(), propertySource.getName()).isNotNull();
assertThat(options.contains(ConfigData.Option.IGNORE_IMPORTS)).isTrue();
assertThat(options.contains(ConfigData.Option.IGNORE_PROFILES)).isTrue();
boolean hasProfile = StringUtils.hasText(loadContext.getResource().getProfile());
assertThat(options.contains(ConfigData.Option.PROFILE_SPECIFIC)).isEqualTo(hasProfile);
return configData;
}));
context = application.run("--spring.application.name=" + APP_NAME,
"--spring.config.import=consul:" + ConsulTestcontainers.getHost() + ":"
+ ConsulTestcontainers.getPort(),

View File

@@ -78,4 +78,11 @@ public class ConsulTestcontainers implements ApplicationContextInitializer<Confi
return new ConsulClient(getHost(), getPort());
}
public static void initializeSystemProperties() {
start();
System.setProperty(ConsulProperties.PREFIX + ".port", getPort().toString());
System.setProperty(ConsulProperties.PREFIX + ".host", getHost());
}
}

View File

@@ -18,17 +18,6 @@
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<!--skip deploy -->
<artifactId>maven-deploy-plugin</artifactId>
@@ -44,22 +33,14 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!--<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-bus</artifactId>
</dependency>-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>

View File

@@ -16,102 +16,14 @@
package org.springframework.cloud.consul.configdatatests;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.serviceregistry.Registration;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
/**
* @author Spencer Gibb
*/
@SpringBootApplication
@RestController
@EnableConfigurationProperties
@Slf4j
public class ConsulConfigDataApplication {
@Autowired
private LoadBalancerClient loadBalancer;
@Autowired
private DiscoveryClient discoveryClient;
@Autowired
private Environment env;
@Autowired
private RestTemplate restTemplate;
@Autowired
private Registration registration;
@Value("${spring.application.name:testConsulApp}")
private String appName;
public static void main(String[] args) {
SpringApplication.run(ConsulConfigDataApplication.class, args);
}
@RequestMapping("/me")
public ServiceInstance me() {
return this.registration;
}
@RequestMapping("/")
public ServiceInstance lb() {
return this.loadBalancer.choose(this.appName);
}
@RequestMapping("/rest")
public String rest() {
return this.restTemplate.getForObject("http://" + this.appName + "/me", String.class);
}
@RequestMapping("/choose")
public String choose() {
return this.loadBalancer.choose(this.appName).getUri().toString();
}
@RequestMapping("/myenv")
public String env(@RequestParam("prop") String prop) {
return this.env.getProperty(prop, "Not Found");
}
@RequestMapping("/prop")
public String prop() {
return sampleProperties().getProp();
}
@RequestMapping("/instances")
public List<ServiceInstance> instances() {
return this.discoveryClient.getInstances(this.appName);
}
@Bean
public SampleProperties sampleProperties() {
return new SampleProperties();
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2018-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.configdatatests;
import java.util.Map;
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.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.cloud.consul.ConsulProperties;
import org.springframework.cloud.consul.config.ConsulConfigProperties;
import org.springframework.cloud.consul.test.ConsulTestcontainers;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@SpringBootTest(classes = ConsulConfigDataApplication.class,
properties = { "spring.application.name=" + ConsulConfigDataOrderingIntegrationTests.APP_NAME,
"spring.config.name=orderingtest", "spring.profiles.active=dev",
"management.endpoints.web.exposure.include=*" },
webEnvironment = RANDOM_PORT)
public class ConsulConfigDataOrderingIntegrationTests {
private static final String BASE_PATH = new WebEndpointProperties().getBasePath();
static final String APP_NAME = "testConsulConfigDataOrderingIntegration";
private static final String PREFIX = "_configDataOrderingIntegrationTests_config__";
private static final String ROOT = PREFIX + UUID.randomUUID();
private static final String VALUE = "my value from consul default profile";
private static final String TEST_PROP = "my.prop";
private static final String KEY = ROOT + "/" + APP_NAME + "/" + TEST_PROP;
private static final String VALUE_PROFILE = "my value from consul dev profile";
private static final String KEY_PROFILE = ROOT + "/" + APP_NAME + ",dev/" + TEST_PROP;
@Autowired
private Environment env;
@BeforeAll
public static void initialize() {
ConsulTestcontainers.initializeSystemProperties();
System.setProperty(ConsulConfigProperties.PREFIX + ".prefix", ROOT);
ConsulClient client = ConsulTestcontainers.client();
client.deleteKVValues(PREFIX);
client.setKVValue(KEY, VALUE);
client.setKVValue(KEY_PROFILE, VALUE_PROFILE);
}
@AfterAll
public static void close() {
System.clearProperty(ConsulProperties.PREFIX + ".port");
System.clearProperty(ConsulProperties.PREFIX + ".host");
System.clearProperty(ConsulProperties.PREFIX + ".prefix");
}
@Test
@SuppressWarnings({ "unchecked", "rawtypes" })
public void contextLoads() {
Integer port = env.getProperty("local.server.port", Integer.class);
ResponseEntity<Map> response = new TestRestTemplate()
.getForEntity("http://localhost:" + port + BASE_PATH + "/env/my.prop", Map.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
Map res = response.getBody();
assertThat(res).containsKey("propertySources");
Map<String, Object> property = (Map<String, Object>) res.get("property");
assertThat(property).containsEntry("value", VALUE_PROFILE);
}
}

View File

@@ -0,0 +1 @@
my.prop=my value from local dev profile