Support for profile specific config data loading (#2260)

Fixes #1922

Co-authored-by: Ryan Baxter <524254+ryanjbaxter@users.noreply.github.com>
This commit is contained in:
Ryan Baxter
2023-05-04 15:31:48 -04:00
committed by GitHub
parent f59f8b5816
commit 6ec9c432cb
7 changed files with 267 additions and 43 deletions

View File

@@ -24,12 +24,12 @@ import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
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.properties.bind.Binder;
@@ -49,6 +49,7 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
@@ -103,6 +104,7 @@ public class ConfigServerConfigDataLoader implements ConfigDataLoader<ConfigServ
}
public ConfigData doLoad(ConfigDataLoaderContext context, ConfigServerConfigDataResource resource) {
ConfigClientProperties properties = resource.getProperties();
List<PropertySource<?>> propertySources = new ArrayList<>();
Exception error = null;
@@ -151,27 +153,15 @@ public class ConfigServerConfigDataLoader implements ConfigDataLoader<ConfigServ
}
else if (ALL_OPTIONS.size() > 2) {
// boot 2.4.5+
return new ConfigData(propertySources, propertySource -> {
String propertySourceName = propertySource.getName();
List<Option> options = new ArrayList<>();
options.add(Option.IGNORE_IMPORTS);
options.add(Option.IGNORE_PROFILES);
// TODO: the profile is now available on the backend
// in a future minor, add the profile associated with a
// PropertySource see
// https://github.com/spring-cloud/spring-cloud-config/issues/1874
for (String profile : resource.getAcceptedProfiles()) {
// TODO: switch to match
// , is used as a profile-separator for property sources
// from vault
// - is the default profile-separator for property sources
if (propertySourceName.matches(".*[-,]" + profile + ".*")) {
// // TODO: switch to Options.with() when implemented
options.add(Option.PROFILE_SPECIFIC);
}
}
return Options.of(options.toArray(new Option[0]));
});
if (resource.isProfileSpecific()) {
List<PropertySource<?>> filteredSources = propertySources.stream()
.filter(propertySource -> resource.getAcceptedProfiles().stream()
.anyMatch(profile -> isProfileSpecificPropertySource(
resource.getProperties().getName(), propertySource, profile)))
.collect(Collectors.toList());
return new ConfigData(filteredSources, Option.IGNORE_IMPORTS, Option.PROFILE_SPECIFIC);
}
return new ConfigData(propertySources, Option.IGNORE_IMPORTS);
}
}
}
@@ -202,6 +192,36 @@ public class ConfigServerConfigDataLoader implements ConfigDataLoader<ConfigServ
return null;
}
private boolean isProfileSpecificPropertySource(String applicationName, PropertySource<?> propertySource,
String profile) {
// Application names can have - so before checking if the application name
// contains a -
// check to see if the application name matches the property source name, if so
// its not a profile specific property source.
// TODO: switch to match
// , is used as a profile-separator for property sources
// from vault
// - is the default profile-separator for property sources
return !applicationName.equals(extractApplicationName(propertySource))
&& propertySource.getName().matches(".*[-,]" + profile + ".*");
}
private String extractApplicationName(PropertySource<?> propertySource) {
if (ObjectUtils.isEmpty(propertySource.getName())) {
return "";
}
int lastSlash = propertySource.getName().lastIndexOf("/");
int lastPeriod = propertySource.getName().lastIndexOf(".");
if (lastSlash == -1) {
lastSlash = 0;
}
if (lastPeriod == -1) {
lastPeriod = propertySource.getName().length();
}
return propertySource.getName().substring(lastSlash + 1, lastPeriod);
}
protected void log(Environment result) {
if (logger.isInfoEnabled()) {
logger.info(String.format("Located environment: name=%s, profiles=%s, label=%s, version=%s, state=%s",

View File

@@ -17,7 +17,6 @@
package org.springframework.cloud.config.client;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
@@ -40,6 +39,7 @@ import org.springframework.cloud.client.ServiceInstance;
import org.springframework.core.Ordered;
import org.springframework.core.log.LogMessage;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
@@ -157,7 +157,7 @@ public class ConfigServerConfigDataLocationResolver
public List<ConfigServerConfigDataResource> resolve(ConfigDataLocationResolverContext context,
ConfigDataLocation location)
throws ConfigDataLocationNotFoundException, ConfigDataResourceNotFoundException {
return Collections.emptyList();
return resolveProfileSpecific(context, location, null);
}
@Override
@@ -188,6 +188,7 @@ public class ConfigServerConfigDataLocationResolver
ConfigServerConfigDataResource resource = new ConfigServerConfigDataResource(properties, location.isOptional(),
profiles);
resource.setProfileSpecific(!ObjectUtils.isEmpty(profiles));
resource.setLog(log);
resource.setRetryProperties(propertyHolder.retryProperties);

View File

@@ -16,6 +16,7 @@
package org.springframework.cloud.config.client;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@@ -38,6 +39,8 @@ public class ConfigServerConfigDataResource extends ConfigDataResource {
private Log log;
private boolean isProfileSpecific = false;
public ConfigServerConfigDataResource(ConfigClientProperties properties, boolean optional, Profiles profiles) {
this.properties = properties;
this.optional = optional;
@@ -48,6 +51,14 @@ public class ConfigServerConfigDataResource extends ConfigDataResource {
return this.properties;
}
public boolean isProfileSpecific() {
return isProfileSpecific;
}
public void setProfileSpecific(boolean profileSpecific) {
isProfileSpecific = profileSpecific;
}
public boolean isOptional() {
return this.optional;
}
@@ -61,6 +72,10 @@ public class ConfigServerConfigDataResource extends ConfigDataResource {
}
List<String> getAcceptedProfiles() {
if (profiles == null) {
return Collections.singletonList(!properties.getProfile().equals(ConfigClientProperties.DEFAULT_PROFILE)
? properties.getProfile() : ConfigClientProperties.DEFAULT_PROFILE);
}
return this.profiles.getAccepted();
}
@@ -101,7 +116,7 @@ public class ConfigServerConfigDataResource extends ConfigDataResource {
@Override
public String toString() {
return new ToStringCreator(this).append("uris", properties.getUri()).append("optional", optional)
.append("profiles", profiles.getAccepted()).toString();
.append("profiles", getAcceptedProfiles()).toString();
}

View File

@@ -16,6 +16,7 @@
package org.springframework.cloud.config.client;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@@ -23,6 +24,8 @@ import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -47,6 +50,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.retry.annotation.Retryable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
@@ -75,10 +79,37 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
this.defaultProperties = defaultProperties;
}
/**
* Combine properties from the config client properties and the active profiles from
* the environment.
* @param properties config client properties,
* @param environment application environment.
* @return A list of combined profiles.
*/
private List<String> combineProfiles(ConfigClientProperties properties,
org.springframework.core.env.Environment environment) {
List<String> combinedProfiles = new ArrayList<>();
if (!ObjectUtils.isEmpty(properties.getProfile())) {
combinedProfiles = Stream.of(properties.getProfile().split(",")).map(String::trim).filter(s -> !s.isEmpty())
.collect(Collectors.toList());
}
if (environment.getActiveProfiles().length > 0) {
List<String> finalCombinedProfiles = combinedProfiles;
List<String> filteredActiveProfiles = Stream.of(environment.getActiveProfiles())
.filter(s -> !finalCombinedProfiles.contains(s)).collect(Collectors.toList());
combinedProfiles.addAll(filteredActiveProfiles);
}
else if (environment.getDefaultProfiles().length > 0 && combinedProfiles.isEmpty()) {
combinedProfiles = Arrays.asList(environment.getDefaultProfiles());
}
return combinedProfiles;
}
@Override
@Retryable(interceptor = "configServerRetryInterceptor")
public org.springframework.core.env.PropertySource<?> locate(org.springframework.core.env.Environment environment) {
ConfigClientProperties properties = this.defaultProperties.override(environment);
properties.setProfile(String.join(",", combineProfiles(properties, environment)));
if (StringUtils.startsWithIgnoreCase(properties.getName(), "application-")) {
InvalidApplicationNameException exception = new InvalidApplicationNameException(properties.getName());

View File

@@ -113,14 +113,19 @@ public class ConfigServerConfigDataCustomizationIntegrationTests {
hasBinder = context.getBinder() != null;
ConfigData configData = context.getInvocation().apply(context.getLoaderContext(), context.getResource());
assertThat(configData).as("ConfigData was null for location %s", context.getResource()).isNotNull();
assertThat(configData.getPropertySources()).hasSize(1);
PropertySource<?> propertySource = configData.getPropertySources().iterator().next();
Options options = configData.getOptions(propertySource);
assertThat(options).as("ConfigData.options was null for location %s property source %s",
context.getResource(), propertySource.getName()).isNotNull();
assertThat(options.contains(Option.IGNORE_IMPORTS)).isTrue();
assertThat(options.contains(Option.IGNORE_PROFILES)).isTrue();
assertThat(options.contains(Option.PROFILE_SPECIFIC)).isFalse();
if (!context.getResource().isProfileSpecific()) {
assertThat(configData.getPropertySources()).hasSize(1);
PropertySource<?> propertySource = configData.getPropertySources().iterator().next();
Options options = configData.getOptions(propertySource);
assertThat(options).as("ConfigData.options was null for location %s property source %s",
context.getResource(), propertySource.getName()).isNotNull();
assertThat(options.contains(Option.IGNORE_IMPORTS)).isTrue();
assertThat(options.contains(Option.PROFILE_SPECIFIC)).isFalse();
}
else {
assertThat(configData.getPropertySources()).hasSize(0);
}
return configData;
}

View File

@@ -0,0 +1,124 @@
/*
* Copyright 2013-2023 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.config.client;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import org.apache.commons.logging.Log;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatchers;
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.context.config.ConfigData;
import org.springframework.boot.context.config.ConfigDataLoaderContext;
import org.springframework.boot.context.config.Profiles;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.cloud.config.environment.PropertySource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.ObjectUtils;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* @author Ryan Baxter
*/
class ConfigServerConfigDataLoaderTests {
@Test
void nonProfileSpecific() {
PropertySource p1 = new PropertySource("p1", new HashMap<>());
PropertySource p2 = new PropertySource("p2", new HashMap<>());
ConfigData configData = setupConfigServerConfigDataLoader(Arrays.asList(p1, p2), "application-slash", null);
assertThat(configData.getPropertySources().size()).isEqualTo(3);
assertThat(configData.getOptions(configData.getPropertySources().get(0))
.contains(ConfigData.Option.IGNORE_IMPORTS)).isTrue();
assertThat(configData.getOptions(configData.getPropertySources().get(1))
.contains(ConfigData.Option.IGNORE_IMPORTS)).isTrue();
assertThat(configData.getOptions(configData.getPropertySources().get(2))
.contains(ConfigData.Option.IGNORE_IMPORTS)).isTrue();
}
@Test
void filterPropertySourcesThatAreNotProfileSpecific() {
PropertySource p1 = new PropertySource("p1", Collections.singletonMap("foo", "bar"));
PropertySource p2 = new PropertySource("p2", Collections.singletonMap("hello", "world"));
ConfigData configData = setupConfigServerConfigDataLoader(Arrays.asList(p1, p2), "application-slash", "dev");
assertThat(configData.getPropertySources().size()).isEqualTo(0);
}
@Test
void returnPropertySourcesThatAreProfileSpecific() {
PropertySource p1 = new PropertySource("p1-dev", Collections.singletonMap("foo", "bar"));
PropertySource p2 = new PropertySource("p2-dev", Collections.singletonMap("hello", "world"));
List<PropertySource> propertySources = Arrays.asList(p1, p2);
ConfigData configData = setupConfigServerConfigDataLoader(propertySources, "application-slash", "dev");
assertThat(configData.getPropertySources().size()).isEqualTo(2);
assertThat(configData.getOptions(configData.getPropertySources().get(0))
.contains(ConfigData.Option.IGNORE_IMPORTS)).isTrue();
assertThat(configData.getOptions(configData.getPropertySources().get(1))
.contains(ConfigData.Option.IGNORE_IMPORTS)).isTrue();
}
private ConfigData setupConfigServerConfigDataLoader(List<PropertySource> propertySources, String applicationName,
String... profileList) {
RestTemplate rest = mock(RestTemplate.class);
Environment environment = new Environment("test", profileList);
environment.addAll(propertySources);
ResponseEntity<Environment> responseEntity = mock(ResponseEntity.class);
when(responseEntity.getStatusCode()).thenReturn(HttpStatus.OK);
when(responseEntity.getBody()).thenReturn(environment);
when(rest.exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(Environment.class),
eq(applicationName), ArgumentMatchers.<String>any())).thenReturn(responseEntity);
ConfigurableBootstrapContext bootstrapContext = mock(ConfigurableBootstrapContext.class);
when(bootstrapContext.get(eq(ConfigClientRequestTemplateFactory.class)))
.thenReturn(mock(ConfigClientRequestTemplateFactory.class));
when(bootstrapContext.get(eq(RestTemplate.class))).thenReturn(rest);
ConfigServerConfigDataLoader loader = new ConfigServerConfigDataLoader(destination -> mock(Log.class));
ConfigDataLoaderContext context = mock(ConfigDataLoaderContext.class);
when(context.getBootstrapContext()).thenReturn(bootstrapContext);
ConfigClientProperties properties = new ConfigClientProperties();
properties.setName(applicationName);
Profiles profiles = mock(Profiles.class);
when(profiles.getAccepted())
.thenReturn(profileList == null ? Collections.singletonList("default") : Arrays.asList(profileList));
ConfigServerConfigDataResource resource = new ConfigServerConfigDataResource(properties, false, profiles);
resource.setProfileSpecific(!ObjectUtils.isEmpty(profileList));
return loader.doLoad(context, resource);
}
}

View File

@@ -73,33 +73,52 @@ public class ConfigServerConfigDataLocationResolverTests {
@Test
void defaultSpringProfiles() {
ConfigServerConfigDataResource resource = testResolveProvileSpecific();
ConfigServerConfigDataResource resource = testResolveProfileSpecific();
assertThat(resource.getProfiles()).isEqualTo("default");
}
@Test
void configClientProfilesOverridesSpringProfilesActive() {
this.environment.setProperty(ConfigClientProperties.PREFIX + ".profile", "myprofile");
ConfigServerConfigDataResource resource = testResolveProvileSpecific();
ConfigServerConfigDataResource resource = testResolveProfileSpecific();
assertThat(resource.getProfiles()).isEqualTo("myprofile");
}
@Test
void configClientProfilesAcceptedProfiles() {
this.environment.setProperty(ConfigClientProperties.PREFIX + ".profile", "myprofile");
ConfigServerConfigDataResource resource = testResolve();
assertThat(resource.getAcceptedProfiles()).contains("myprofile");
}
@Test
void configClientProfilesDefaultAcceptedProfiles() {
ConfigServerConfigDataResource resource = testResolve();
assertThat(resource.getAcceptedProfiles()).contains("default");
}
@Test
void configClientSpringProfilesActiveOverridesDefaultClientProfiles() {
ConfigServerConfigDataResource resource = testResolveProvileSpecific("myactiveprofile");
ConfigServerConfigDataResource resource = testResolveProfileSpecific("myactiveprofile");
assertThat(resource.getProfiles()).isEqualTo("myactiveprofile");
}
@Test
void assertConfigDataResourceHasNullProfiles() {
ConfigServerConfigDataResource resource = testResolve();
assertThat(resource.isProfileSpecific()).isFalse();
}
@Test
void configNameDefaultsToApplication() {
ConfigServerConfigDataResource resource = testResolveProvileSpecific();
ConfigServerConfigDataResource resource = testResolveProfileSpecific();
assertThat(resource.getProperties().getName()).isEqualTo("application");
}
@Test
void configNameDefaultsToSpringApplicationName() {
this.environment.setProperty("spring.application.name", "myapp");
ConfigServerConfigDataResource resource = testResolveProvileSpecific();
ConfigServerConfigDataResource resource = testResolveProfileSpecific();
assertThat(resource.getProperties().getName()).isEqualTo("myapp");
}
@@ -107,13 +126,13 @@ public class ConfigServerConfigDataLocationResolverTests {
void configNameOverridesSpringApplicationName() {
this.environment.setProperty("spring.application.name", "myapp");
this.environment.setProperty(ConfigClientProperties.PREFIX + ".name", "myconfigname");
ConfigServerConfigDataResource resource = testResolveProvileSpecific();
ConfigServerConfigDataResource resource = testResolveProfileSpecific();
assertThat(resource.getProperties().getName()).isEqualTo("myconfigname");
}
@Test
void retryPropertiesShouldBeDefaultByDefault() {
ConfigServerConfigDataResource resource = testResolveProvileSpecific();
ConfigServerConfigDataResource resource = testResolveProfileSpecific();
RetryProperties defaultRetry = new RetryProperties();
assertThat(resource.getRetryProperties().getMaxAttempts()).isEqualTo(defaultRetry.getMaxAttempts());
assertThat(resource.getRetryProperties().getMaxInterval()).isEqualTo(defaultRetry.getMaxInterval());
@@ -211,11 +230,11 @@ public class ConfigServerConfigDataLocationResolverTests {
return resources.get(0);
}
private ConfigServerConfigDataResource testResolveProvileSpecific() {
return testResolveProvileSpecific("default");
private ConfigServerConfigDataResource testResolveProfileSpecific() {
return testResolveProfileSpecific("default");
}
private ConfigServerConfigDataResource testResolveProvileSpecific(String activeProfile) {
private ConfigServerConfigDataResource testResolveProfileSpecific(String activeProfile) {
when(context.getBootstrapContext()).thenReturn(mock(ConfigurableBootstrapContext.class));
Profiles profiles = mock(Profiles.class);
if (activeProfile != null) {
@@ -228,4 +247,13 @@ public class ConfigServerConfigDataLocationResolverTests {
return resources.get(0);
}
private ConfigServerConfigDataResource testResolve() {
when(context.getBootstrapContext()).thenReturn(mock(ConfigurableBootstrapContext.class));
List<ConfigServerConfigDataResource> resources = this.resolver.resolve(context,
ConfigDataLocation.of("configserver:"));
assertThat(resources).hasSize(1);
return resources.get(0);
}
}