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-287
This commit is contained in:
spencergibb
2021-05-10 16:10:55 -04:00
parent ec845a47a0
commit b00c109c55
8 changed files with 254 additions and 16 deletions

View File

@@ -16,16 +16,28 @@
package org.springframework.cloud.zookeeper.config;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.curator.framework.CuratorFramework;
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.ConfigDataResourceNotFoundException;
import org.springframework.util.StringUtils;
public class ZookeeperConfigDataLoader implements ConfigDataLoader<ZookeeperConfigDataResource> {
private static final EnumSet<ConfigData.Option> ALL_OPTIONS = EnumSet.allOf(ConfigData.Option.class);
private final Log log;
public ZookeeperConfigDataLoader(Log log) {
this.log = log;
}
@Override
public ConfigData load(ConfigDataLoaderContext context, ZookeeperConfigDataResource resource) {
@@ -33,11 +45,36 @@ public class ZookeeperConfigDataLoader implements ConfigDataLoader<ZookeeperConf
CuratorFramework curator = context.getBootstrapContext().get(CuratorFramework.class);
ZookeeperPropertySource propertySource = new ZookeeperPropertySource(resource.getContext(),
curator);
return new ConfigData(Collections.singletonList(propertySource));
List<ZookeeperPropertySource> 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, ConfigData.Option.IGNORE_IMPORTS, ConfigData.Option.IGNORE_PROFILES);
}
else if (ALL_OPTIONS.size() > 2) {
// boot 2.4.5+
return new ConfigData(propertySources, source -> {
List<ConfigData.Option> options = new ArrayList<>();
options.add(ConfigData.Option.IGNORE_IMPORTS);
options.add(ConfigData.Option.IGNORE_PROFILES);
if (StringUtils.hasText(resource.getProfile())) {
options.add(ConfigData.Option.PROFILE_SPECIFIC);
}
return ConfigData.Options.of(options.toArray(new ConfigData.Option[0]));
});
}
}
catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug("Error getting properties from consul: " + resource, e);
}
throw new ConfigDataResourceNotFoundException(resource, e);
}
return null;
}
}

View File

@@ -21,6 +21,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
@@ -35,6 +36,7 @@ import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.cloud.zookeeper.CuratorFactory;
import org.springframework.cloud.zookeeper.ZookeeperProperties;
import org.springframework.cloud.zookeeper.config.ZookeeperPropertySources.Context;
import org.springframework.core.env.MapPropertySource;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
@@ -88,20 +90,21 @@ public class ZookeeperConfigDataLocationResolver implements ConfigDataLocationRe
ZookeeperPropertySources sources = new ZookeeperPropertySources(properties, log);
List<String> contexts = (locationUri == null || CollectionUtils.isEmpty(locationUri.getPathSegments()))
? sources.getAutomaticContexts(profiles.getAccepted(), false) : getCustomContexts(locationUri);
List<Context> contexts = (locationUri == null || CollectionUtils.isEmpty(locationUri.getPathSegments()))
? sources.generateAutomaticContexts(profiles.getAccepted(), false) : getCustomContexts(locationUri);
// promote beans to context
context.getBootstrapContext().addCloseListener(event -> {
HashMap<String, Object> source = new HashMap<>();
source.put("spring.cloud.zookeeper.config.property-source-contexts", contexts);
source.put("spring.cloud.zookeeper.config.property-source-contexts", contexts.stream().map(Context::getPath).collect(Collectors.toList()));
MapPropertySource propertySource = new MapPropertySource("zookeeperConfigData", source);
event.getApplicationContext().getEnvironment().getPropertySources().addFirst(propertySource);
});
ArrayList<ZookeeperConfigDataResource> locations = new ArrayList<>();
contexts.forEach(propertySourceContext -> locations
.add(new ZookeeperConfigDataResource(propertySourceContext, location.isOptional())));
.add(new ZookeeperConfigDataResource(propertySourceContext.getPath(), location.isOptional(), propertySourceContext
.getProfile())));
return locations;
}
@@ -110,12 +113,12 @@ public class ZookeeperConfigDataLocationResolver implements ConfigDataLocationRe
return context.getBootstrapContext().getOrElse(BindHandler.class, null);
}
protected List<String> getCustomContexts(UriComponents uriComponents) {
protected List<Context> getCustomContexts(UriComponents uriComponents) {
if (!StringUtils.hasLength(uriComponents.getPath())) {
return Collections.emptyList();
}
return Arrays.asList(uriComponents.getPath().split(";"));
return Arrays.stream(uriComponents.getPath().split(";")).map(Context::new).collect(Collectors.toList());
}
@Nullable

View File

@@ -25,10 +25,17 @@ public class ZookeeperConfigDataResource extends ConfigDataResource {
private final String context;
private final boolean optional;
private final String profile;
public ZookeeperConfigDataResource(String context, boolean optional) {
public ZookeeperConfigDataResource(String context, boolean optional, String profile) {
this.context = context;
this.optional = optional;
this.profile = profile;
}
@Deprecated
public ZookeeperConfigDataResource(String context, boolean optional) {
this(context, optional, null);
}
public String getContext() {
@@ -39,6 +46,10 @@ public class ZookeeperConfigDataResource extends ConfigDataResource {
return this.optional;
}
public String getProfile() {
return this.profile;
}
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -48,13 +59,12 @@ public class ZookeeperConfigDataResource extends ConfigDataResource {
return false;
}
ZookeeperConfigDataResource that = (ZookeeperConfigDataResource) 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.optional, this.context);
return Objects.hash(this.optional, this.context, this.profile);
}
@Override
@@ -62,6 +72,7 @@ public class ZookeeperConfigDataResource extends ConfigDataResource {
return new ToStringCreator(this)
.append("context", context)
.append("optional", optional)
.append("profile", profile)
.toString();
}

View File

@@ -19,10 +19,13 @@ package org.springframework.cloud.zookeeper.config;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.curator.framework.CuratorFramework;
import org.springframework.core.style.ToStringCreator;
public class ZookeeperPropertySources {
private final ZookeeperConfigProperties properties;
private final Log log;
@@ -37,11 +40,15 @@ public class ZookeeperPropertySources {
}
public List<String> getAutomaticContexts(List<String> profiles, boolean reverse) {
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<>();
String root = properties.getRoot();
List<String> contexts = new ArrayList<>();
String defaultContext = root + "/" + properties.getDefaultContext();
contexts.add(defaultContext);
contexts.add(new Context(defaultContext));
addProfiles(contexts, defaultContext, profiles);
StringBuilder baseContext = new StringBuilder(root);
@@ -50,7 +57,7 @@ public class ZookeeperPropertySources {
}
// getName() defaults to ${spring.application.name} or application
baseContext.append(properties.getName());
contexts.add(baseContext.toString());
contexts.add(new Context(baseContext.toString()));
addProfiles(contexts, baseContext.toString(), profiles);
if (reverse) {
@@ -59,9 +66,10 @@ public class ZookeeperPropertySources {
return contexts;
}
private void addProfiles(List<String> contexts, String baseContext, List<String> profiles) {
private void addProfiles(List<Context> contexts, String baseContext, List<String> profiles) {
for (String profile : profiles) {
contexts.add(baseContext + properties.getProfileSeparator() + profile);
String path = baseContext + properties.getProfileSeparator() + profile;
contexts.add(new Context(path, profile));
}
}
@@ -80,6 +88,38 @@ public class ZookeeperPropertySources {
}
return null;
}
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 ZookeeperPropertySourceNotFoundException extends RuntimeException {

View File

@@ -67,6 +67,13 @@
<artifactId>curator-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-zookeeper-core</artifactId>
<version>${project.version}</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,138 @@
/*
* Copyright 2018-2021 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.zookeeper.sample;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.imps.CuratorFrameworkState;
import org.apache.curator.retry.RetryOneTime;
import org.apache.zookeeper.KeeperException;
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.zookeeper.ZookeeperProperties;
import org.springframework.cloud.zookeeper.config.ZookeeperConfigProperties;
import org.springframework.cloud.zookeeper.test.ZookeeperTestingServer;
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 = SampleZookeeperApplication.class,
properties = { "spring.application.name=" + ZookeeperConfigDataOrderingIntegrationTests.APP_NAME,
"spring.config.name=orderingtest", "spring.profiles.active=dev",
"management.endpoints.web.exposure.include=*" },
webEnvironment = RANDOM_PORT)
public class ZookeeperConfigDataOrderingIntegrationTests {
private static final String BASE_PATH = new WebEndpointProperties().getBasePath();
static final String APP_NAME = "testZkConfigDataOrderingIntegration";
private static final String PREFIX = "_configDataOrderingIntegrationTests_config__";
private static final String ROOT = "/" + PREFIX + UUID.randomUUID();
private static final String VALUE = "my value from zk 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 zk dev profile";
private static final String KEY_PROFILE = ROOT + "/" + APP_NAME + ",dev/" + TEST_PROP;
private static ZookeeperTestingServer testingServer;
private static CuratorFramework curator;
@Autowired
private Environment env;
@BeforeAll
public static void initialize() throws Exception {
testingServer = new ZookeeperTestingServer();
testingServer.start();
System.setProperty(ZookeeperProperties.PREFIX + ".connect-string", "localhost:" + testingServer.getPort());
System.setProperty(ZookeeperConfigProperties.PREFIX + ".root", ROOT);
String connectString = "localhost:" + testingServer.getPort();
curator = CuratorFrameworkFactory.builder()
.retryPolicy(new RetryOneTime(500)).connectString(connectString).build();
curator.start();
List<String> children = curator.getChildren().forPath("/");
for (String child : children) {
if (child.startsWith(PREFIX) && child.length() > PREFIX.length()) {
delete("/" + child);
}
}
StringBuilder create = new StringBuilder(1024);
create.append(curator.create().creatingParentsIfNeeded()
.forPath(KEY, VALUE.getBytes())).append('\n');
create.append(curator.create().creatingParentsIfNeeded()
.forPath(KEY_PROFILE, VALUE_PROFILE.getBytes())).append('\n');
curator.close();
System.out.println(create);
}
public static void delete(String path) throws Exception {
try {
if (curator.getState() == CuratorFrameworkState.STARTED) {
curator.delete().deletingChildrenIfNeeded().forPath(path);
}
}
catch (KeeperException e) {
if (e.code() != KeeperException.Code.NONODE) {
throw e;
}
}
}
@AfterAll
public static void close() throws Exception {
try {
delete(ROOT);
}
finally {
testingServer.close();
}
System.clearProperty(ZookeeperProperties.PREFIX + ".connect-string");
}
@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

View File

@@ -0,0 +1 @@
spring.config.import=zookeeper: