Merge remote-tracking branch 'origin/main' into native-image-for-server

This commit is contained in:
Olga MaciaszekSharma
2023-11-30 17:23:15 +01:00
14 changed files with 221 additions and 19 deletions

View File

@@ -93,6 +93,7 @@ First, you need to set `spring.cloud.config.fail-fast=true`.
Then you need to add `spring-retry` and `spring-boot-starter-aop` to your classpath.
The default behavior is to retry six times with an initial backoff interval of 1000ms and an exponential multiplier of 1.1 for subsequent backoffs.
You can configure these properties (and others) by setting the `spring.cloud.config.retry.*` configuration properties.
To use a random exponential backoff policy set `spring.cloud.config.retry.useRandomPolicy` to `true`.
TIP: To take full control of the retry behavior and are using legacy bootstrap, add a `@Bean` of type `RetryOperationsInterceptor` with an ID of `configServerRetryInterceptor`.
Spring Retry has a `RetryInterceptorBuilder` that supports creating one.

View File

@@ -37,5 +37,6 @@ AWS Secrets Manager API credentials are determined using link:https://docs.aws.a
[NOTE]
====
- When no application is specified `application` is the default, and when no profile is specified `default` is used.
- Both `label` and `defaultLabel` properties are ignored, when `ignoreLabel` is set to `true`.
====

View File

@@ -78,3 +78,17 @@ Note that if the default label is not set and a request does not define a label,
Note that if the staging label contains a slash (`/`), then the label in the HTTP URL should instead be specified with the special string `(\{special-string})` (to avoid ambiguity with other URL paths) the same way <<_git_backend,Git backend's section>> describes it.
Use `spring.cloud.config.server.aws-secretsmanager.ignore-label` property to ignore the `{label}` parameter of the HTTP resource as well as `spring.cloud.config.server.aws-secretsmanager.default-label` property. The repository will use secrets as if labelled version support is disabled.
[source,yaml]
----
spring:
profiles:
active: aws-secretsmanager
cloud:
config:
server:
aws-secretsmanager:
region: us-east-1
ignore-label: true
----

View File

@@ -22,6 +22,7 @@
|spring.cloud.config.retry.max-attempts | `+++6+++` | Maximum number of attempts.
|spring.cloud.config.retry.max-interval | `+++2000+++` | Maximum interval for backoff.
|spring.cloud.config.retry.multiplier | `+++1.1+++` | Multiplier for next interval.
|spring.cloud.config.retry.use-random-policy | `+++false+++` | Use a random exponential backoff policy.
|spring.cloud.config.send-state | `+++true+++` | Flag to indicate whether to send state. Default true.
|spring.cloud.config.tls | | TLS properties.
|spring.cloud.config.token | | Security Token passed thru to underlying environment repository.

View File

@@ -265,8 +265,23 @@ public class ConfigServerConfigDataLoader implements ConfigDataLoader<ConfigServ
String name = properties.getName();
String profile = resource.getProfiles();
String token = properties.getToken();
int noOfUrls = properties.getUri().length;
if (noOfUrls > 1) {
String[] uris;
boolean discoveryEnabled = properties.getDiscovery().isEnabled();
ConfigClientProperties bootstrapConfigClientProperties = context.getBootstrapContext()
.get(ConfigClientProperties.class);
// In the case where discovery is enabled we need to extract the config server
// uris, username, and password
// from the properties from the context. These are set in
// ConfigServerInstanceMonitor.refresh which will only
// be called the first time we fetch configuration.
if (discoveryEnabled) {
uris = bootstrapConfigClientProperties.getUri();
}
else {
uris = properties.getUri();
}
int noOfUrls = uris.length;
if (uris.length > 1) {
logger.info("Multiple Config Server Urls found listed.");
}
@@ -284,10 +299,19 @@ public class ConfigServerConfigDataLoader implements ConfigDataLoader<ConfigServ
.get(ConfigClientRequestTemplateFactory.class);
for (int i = 0; i < noOfUrls; i++) {
ConfigClientProperties.Credentials credentials = properties.getCredentials(i);
String uri = credentials.getUri();
String username = credentials.getUsername();
String password = credentials.getPassword();
String username;
String password;
String uri = uris[i];
if (discoveryEnabled) {
password = bootstrapConfigClientProperties.getPassword();
username = bootstrapConfigClientProperties.getUsername();
}
else {
ConfigClientProperties.Credentials credentials = properties.getCredentials(i);
uri = credentials.getUri();
username = credentials.getUsername();
password = credentials.getPassword();
}
logger.info("Fetching config from server at : " + uri);

View File

@@ -51,7 +51,6 @@ 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;
@@ -81,8 +80,7 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
}
/**
* Combine properties from the config client properties and the active profiles from
* the environment.
* Combine the active and default profiles from the environment.
* @param properties config client properties,
* @param environment application environment.
* @return A list of combined profiles.
@@ -90,10 +88,6 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
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())
@@ -110,7 +104,9 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
@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.hasText(properties.getProfile())) {
properties.setProfile(String.join(",", combineProfiles(properties, environment)));
}
if (StringUtils.startsWithIgnoreCase(properties.getName(), "application-")) {
InvalidApplicationNameException exception = new InvalidApplicationNameException(properties.getName());

View File

@@ -50,6 +50,11 @@ public class RetryProperties {
*/
int maxAttempts = 6;
/**
* Use a random exponential backoff policy.
*/
boolean useRandomPolicy = false;
public long getInitialInterval() {
return this.initialInterval;
}
@@ -82,4 +87,12 @@ public class RetryProperties {
this.maxAttempts = maxAttempts;
}
public boolean isUseRandomPolicy() {
return this.useRandomPolicy;
}
public void setUseRandomPolicy(boolean useRandomPolicy) {
this.useRandomPolicy = useRandomPolicy;
}
}

View File

@@ -39,9 +39,9 @@ public final class RetryTemplateFactory {
}
public static RetryTemplate create(RetryProperties properties, Log log) {
RetryTemplate retryTemplate = RetryTemplate.builder().maxAttempts(properties.getMaxAttempts())
.exponentialBackoff(properties.getInitialInterval(), properties.getMultiplier(),
properties.getMaxInterval())
RetryTemplate retryTemplate = RetryTemplate
.builder().maxAttempts(properties.getMaxAttempts()).exponentialBackoff(properties.getInitialInterval(),
properties.getMultiplier(), properties.getMaxInterval(), properties.isUseRandomPolicy())
.build();
try {
field.set(retryTemplate, log);

View File

@@ -412,6 +412,28 @@ public class ConfigServerConfigDataLoaderTests {
}
@Test
public void useDiscoveryUriIfEnabled() throws Exception {
String[] uris = new String[] { "http://uritest:8888" };
properties.setUri(uris);
ConfigClientProperties.Discovery discovery = new ConfigClientProperties.Discovery();
discovery.setEnabled(true);
discovery.setServiceId("configservice");
properties.setDiscovery(discovery);
this.loader = new ConfigServerConfigDataLoader(destination -> logger);
ClientHttpRequestFactory requestFactory = mock(ClientHttpRequestFactory.class);
RestTemplate restTemplate = new RestTemplate(requestFactory);
when(bootstrapContext.get(RestTemplate.class)).thenReturn(restTemplate);
ConfigClientProperties bootstrapConfigClientProperties = new ConfigClientProperties();
bootstrapConfigClientProperties.setDiscovery(discovery);
bootstrapConfigClientProperties.setUri(new String[] { "http://configservice:8888" });
when(bootstrapContext.get(ConfigClientProperties.class)).thenReturn(bootstrapConfigClientProperties);
mockRequestResponse(requestFactory, "http://configservice:8888", HttpStatus.OK);
assertThat(this.loader.load(context, resource)).isNotNull();
}
@Disabled
@Test
// TODO Enable once we have

View File

@@ -138,6 +138,7 @@ public class ConfigServerConfigDataLocationResolverTests {
assertThat(resource.getRetryProperties().getMaxInterval()).isEqualTo(defaultRetry.getMaxInterval());
assertThat(resource.getRetryProperties().getInitialInterval()).isEqualTo(defaultRetry.getInitialInterval());
assertThat(resource.getRetryProperties().getMultiplier()).isEqualTo(defaultRetry.getMultiplier());
assertThat(resource.getRetryProperties().isUseRandomPolicy()).isEqualTo(defaultRetry.isUseRandomPolicy());
}
@Test
@@ -158,6 +159,7 @@ public class ConfigServerConfigDataLocationResolverTests {
assertThat(resource.getRetryProperties().getMaxInterval()).isEqualTo(1500);
assertThat(resource.getRetryProperties().getInitialInterval()).isEqualTo(1100);
assertThat(resource.getRetryProperties().getMultiplier()).isEqualTo(1.2);
assertThat(resource.getRetryProperties().isUseRandomPolicy()).isEqualTo(false);
}
@Test

View File

@@ -118,6 +118,17 @@ public class ConfigServicePropertySourceLocatorTests {
assertThat(this.locator.locateCollection(this.environment)).isNotNull();
}
@Test
public void overrideProfile() {
Environment body = new Environment("app", "override-profile");
body.add(new PropertySource("p1", new HashMap<>()));
mockRequestResponseWithProfile(new ResponseEntity<>(body, HttpStatus.OK), "override-profile");
this.locator.setRestTemplate(this.restTemplate);
TestPropertyValues.of("spring.cloud.config.profile:override-profile", "spring.profiles.active: foo")
.applyTo(this.environment);
assertThat(this.locator.locateCollection(this.environment).size()).isEqualTo(2);
}
@Test
public void sunnyDayWithLabelThatContainsASlash() {
Environment body = new Environment("app", "master");
@@ -571,6 +582,12 @@ public class ConfigServicePropertySourceLocatorTests {
ArgumentMatchers.eq(label))).thenReturn(response);
}
private void mockRequestResponseWithProfile(ResponseEntity<?> response, String profiles) {
Mockito.when(this.restTemplate.exchange(Mockito.any(String.class), Mockito.any(HttpMethod.class),
Mockito.any(HttpEntity.class), Mockito.any(Class.class), anyString(), ArgumentMatchers.eq(profiles)))
.thenReturn(response);
}
@SuppressWarnings("unchecked")
private void mockRequestResponseWithoutLabel(ResponseEntity<?> response) {
Mockito.when(this.restTemplate.exchange(Mockito.any(String.class), Mockito.any(HttpMethod.class),

View File

@@ -54,6 +54,11 @@ public class AwsSecretsManagerEnvironmentProperties implements EnvironmentReposi
*/
private String defaultLabel;
/**
* Do not set staging label when fetching the secret values.
*/
private boolean ignoreLabel;
/**
* The order of the environment repository.
*/
@@ -105,6 +110,14 @@ public class AwsSecretsManagerEnvironmentProperties implements EnvironmentReposi
this.defaultLabel = defaultLabel;
}
public boolean isIgnoreLabel() {
return this.ignoreLabel;
}
public void setIgnoreLabel(boolean ignoreLabel) {
this.ignoreLabel = ignoreLabel;
}
public int getOrder() {
return order;
}

View File

@@ -28,6 +28,7 @@ import org.apache.commons.logging.LogFactory;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest;
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse;
import software.amazon.awssdk.services.secretsmanager.model.InvalidRequestException;
import software.amazon.awssdk.services.secretsmanager.model.ResourceNotFoundException;
import org.springframework.cloud.config.environment.Environment;
@@ -72,6 +73,7 @@ public class AwsSecretsManagerEnvironmentRepository implements EnvironmentReposi
final String defaultApplication = configServerProperties.getDefaultApplicationName();
final String defaultProfile = configServerProperties.getDefaultProfile();
final String defaultLabel = environmentProperties.getDefaultLabel();
final boolean ignoreLabel = environmentProperties.isIgnoreLabel();
if (ObjectUtils.isEmpty(application)) {
application = defaultApplication;
@@ -81,7 +83,10 @@ public class AwsSecretsManagerEnvironmentRepository implements EnvironmentReposi
profileList = defaultProfile;
}
if (StringUtils.isEmpty(label)) {
if (ignoreLabel) {
label = null;
}
else if (StringUtils.isEmpty(label)) {
label = defaultLabel;
}
@@ -155,7 +160,7 @@ public class AwsSecretsManagerEnvironmentRepository implements EnvironmentReposi
}
}
}
catch (ResourceNotFoundException | IOException e) {
catch (InvalidRequestException | ResourceNotFoundException | IOException e) {
log.debug(String.format(
"Skip adding propertySource. Unable to load secrets from AWS Secrets Manager for secretId=%s",
path), e);

View File

@@ -42,6 +42,7 @@ import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.awssdk.services.secretsmanager.model.CreateSecretRequest;
import software.amazon.awssdk.services.secretsmanager.model.CreateSecretResponse;
import software.amazon.awssdk.services.secretsmanager.model.DeleteSecretRequest;
import software.amazon.awssdk.services.secretsmanager.model.RestoreSecretRequest;
import software.amazon.awssdk.services.secretsmanager.model.UpdateSecretVersionStageRequest;
import org.springframework.cloud.config.environment.Environment;
@@ -87,10 +88,19 @@ public class AwsSecretsManagerEnvironmentRepositoryTests {
private final AwsSecretsManagerEnvironmentRepository labeledRepository = new AwsSecretsManagerEnvironmentRepository(
smClient, configServerProperties, labeledEnvironmentProperties);
private final AwsSecretsManagerEnvironmentProperties ignoreLabelEnvironmentProperties = new AwsSecretsManagerEnvironmentProperties() {{
setIgnoreLabel(true);
}};
private final AwsSecretsManagerEnvironmentRepository ignoreLabelRepository = new AwsSecretsManagerEnvironmentRepository(
smClient, configServerProperties, ignoreLabelEnvironmentProperties);
private final ObjectMapper objectMapper = new ObjectMapper().configure(SerializationFeature.INDENT_OUTPUT, true);
private final List<String> toBeRemoved = new ArrayList<>();
private final List<String> markedForDeletion = new ArrayList<>();
private static Map<String, String> getFooProperties() {
return new HashMap<String, String>() {
{
@@ -237,6 +247,10 @@ public class AwsSecretsManagerEnvironmentRepositoryTests {
@AfterEach
public void cleanUp() {
markedForDeletion
.forEach(value -> smClient.restoreSecret(RestoreSecretRequest.builder().secretId(value).build()));
markedForDeletion.clear();
toBeRemoved.forEach(value -> smClient
.deleteSecret(DeleteSecretRequest.builder().secretId(value).forceDeleteWithoutRecovery(true).build()));
toBeRemoved.clear();
@@ -1820,6 +1834,47 @@ public class AwsSecretsManagerEnvironmentRepositoryTests {
assertThat(resultEnv).usingRecursiveComparison().withStrictTypeChecking().isEqualTo(expectedEnv);
}
@Test
public void testFindOneWithExistingApplicationAndExistingProfileAndExistingLabelWhenIgnoreLabelIsSet() {
String application = "foo";
String profile = "prod";
String label = "release";
String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
String fooProdPropertiesName = "aws:secrets:/secret/foo-prod/";
PropertySource fooProdProperties = new PropertySource(fooProdPropertiesName, getFooProdReleaseProperties());
String fooPropertiesName = "aws:secrets:/secret/foo/";
PropertySource fooProperties = new PropertySource(fooPropertiesName, getFooReleaseProperties());
String fooDefaultPropertiesName = "aws:secrets:/secret/foo-default/";
PropertySource fooDefaultProperties = new PropertySource(fooDefaultPropertiesName,
getFooDefaultReleaseProperties());
String applicationProdPropertiesName = "aws:secrets:/secret/application-prod/";
PropertySource applicationProdProperties = new PropertySource(applicationProdPropertiesName,
getApplicationProdReleaseProperties());
String applicationDefaultPropertiesName = "aws:secrets:/secret/application-default/";
PropertySource applicationDefaultProperties = new PropertySource(applicationDefaultPropertiesName,
getApplicationDefaultReleaseProperties());
String applicationPropertiesName = "aws:secrets:/secret/application/";
PropertySource applicationProperties = new PropertySource(applicationPropertiesName,
getApplicationReleaseProperties());
Environment expectedEnv = new Environment(application, profiles, null, null, null);
expectedEnv.addAll(Arrays.asList(
fooProdProperties, applicationProdProperties, fooDefaultProperties,
applicationDefaultProperties, fooProperties, applicationProperties));
putSecrets(expectedEnv);
Environment resultEnv = ignoreLabelRepository.findOne(application, profile, label);
assertThat(resultEnv).usingRecursiveComparison().withStrictTypeChecking().isEqualTo(expectedEnv);
}
@Test
public void testFindOneWithNullApplicationAndNullProfile() {
String application = null;
@@ -2502,6 +2557,36 @@ public class AwsSecretsManagerEnvironmentRepositoryTests {
assertThat(resultEnv).usingRecursiveComparison().withStrictTypeChecking().isEqualTo(environment);
}
@Test
public void testFindOneWithExistingApplicationAndNonExistingProfileAndNoDefaultProfileForFooMarkedForDeletion() {
String application = "foo";
String profile = randomAlphabetic(RandomUtils.nextInt(2, 25));
String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
String fooPropertiesName = "aws:secrets:/secret/foo/";
PropertySource fooProperties = new PropertySource(fooPropertiesName, getFooProperties());
String applicationDefaultPropertiesName = "aws:secrets:/secret/application-default/";
PropertySource applicationDefaultProperties = new PropertySource(applicationDefaultPropertiesName,
getApplicationDefaultProperties());
String applicationPropertiesName = "aws:secrets:/secret/application/";
PropertySource applicationProperties = new PropertySource(applicationPropertiesName,
getApplicationProperties());
Environment environment = new Environment(application, profiles, null, null, null);
environment.addAll(Arrays.asList(applicationDefaultProperties, fooProperties, applicationProperties));
putSecrets(environment);
deleteSecrets(environment);
Environment emptyEnvironment = new Environment(application, profiles, null, null, null);
Environment resultEnv = repository.findOne(application, profile, null);
assertThat(resultEnv).usingRecursiveComparison().withStrictTypeChecking().isEqualTo(emptyEnvironment);
}
@Test
public void factoryCustomizableWithRegion() {
AwsSecretsManagerEnvironmentRepositoryFactory factory = new AwsSecretsManagerEnvironmentRepositoryFactory(
@@ -2539,6 +2624,14 @@ public class AwsSecretsManagerEnvironmentRepositoryTests {
}
}
private void deleteSecrets(Environment environment) {
for (PropertySource ps : environment.getPropertySources()) {
String path = StringUtils.delete(ps.getName(), environmentProperties.getOrigin());
smClient.deleteSecret(DeleteSecretRequest.builder().secretId(path).recoveryWindowInDays(30L).build());
markedForDeletion.add(path);
}
}
private String getSecrets(PropertySource ps) {
Map<String, String> map = (Map<String, String>) ps.getSource();
try {