Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow loading of config maps to be retried on failure #648

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions spring-cloud-kubernetes-config/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@
<artifactId>spring-security-rsa</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,22 @@

package org.springframework.cloud.kubernetes.config;

import java.util.Optional;

import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.client.KubernetesClient;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.kubernetes.KubernetesAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.retry.support.RetryTemplate;

/**
* Auto configuration that reuses Kubernetes config maps as property sources.
Expand All @@ -49,8 +53,10 @@ protected static class KubernetesPropertySourceConfiguration {

@Bean
@ConditionalOnProperty(name = "spring.cloud.kubernetes.config.enabled", matchIfMissing = true)
public ConfigMapPropertySourceLocator configMapPropertySourceLocator(ConfigMapConfigProperties properties) {
return new ConfigMapPropertySourceLocator(this.client, properties);
public ConfigMapPropertySourceLocator configMapPropertySourceLocator(ConfigMapConfigProperties properties,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of the Optional here I think it might be cleaner if you used @ConditionalOnMissingClass

Optional<ConfigMapRetryTemplateFactory> configMapRetryTemplateFactory) {
return new ConfigMapPropertySourceLocator(this.client, properties,
configMapRetryTemplateFactory.orElse(null));
}

@Bean
Expand All @@ -59,6 +65,15 @@ public SecretsPropertySourceLocator secretsPropertySourceLocator(SecretsConfigPr
return new SecretsPropertySourceLocator(this.client, properties);
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnClass(RetryTemplate.class)
public ConfigMapRetryTemplateFactory configMapRetryPolicyFactory(ConfigMapConfigProperties properties) {
final RetryTemplate template = RetryTemplate.builder().maxAttempts(properties.getRetry().getMaxAttempts())
.fixedBackoff(properties.getRetry().getBackoff().toMillis()).retryOn(Exception.class).build();
return () -> template;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package org.springframework.cloud.kubernetes.config;

import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
Expand All @@ -27,6 +28,7 @@
* Config map configuration properties.
*
* @author Ioannis Canellos
* @author Roy Jacobs
*/
@ConfigurationProperties("spring.cloud.kubernetes.config")
public class ConfigMapConfigProperties extends AbstractConfigProperties {
Expand All @@ -39,6 +41,8 @@ public class ConfigMapConfigProperties extends AbstractConfigProperties {

private List<Source> sources = Collections.emptyList();

private Retry retry = new Retry();

public boolean isEnableApi() {
return this.enableApi;
}
Expand All @@ -63,6 +67,14 @@ public void setSources(List<Source> sources) {
this.sources = sources;
}

public Retry getRetry() {
return retry;
}

public void setRetry(Retry retry) {
this.retry = retry;
}

/**
* @return A list of Source to use If the user has not specified any Source
* properties, then a single Source is constructed based on the supplied name and
Expand All @@ -73,9 +85,8 @@ public void setSources(List<Source> sources) {
*/
public List<NormalizedSource> determineSources() {
if (this.sources.isEmpty()) {
return Collections.singletonList(
new NormalizedSource(ConfigMapConfigProperties.this.name,
ConfigMapConfigProperties.this.namespace));
return Collections.singletonList(new NormalizedSource(ConfigMapConfigProperties.this.name,
ConfigMapConfigProperties.this.namespace));
}

return this.sources.stream().map(s -> s.normalize(this.name, this.namespace)).collect(Collectors.toList());
Expand Down Expand Up @@ -138,6 +149,39 @@ public NormalizedSource normalize(String defaultName, String defaultNamespace) {

}

public static class Retry {

/**
* The amount of times to try contacting Kubernetes to load config maps.
*/
private int maxAttempts = 3;

/**
* The delay in between attempts to load config maps.
*/
private Duration backoff = Duration.ofSeconds(1);

public Retry() {
}

public int getMaxAttempts() {
return maxAttempts;
}

public void setMaxAttempts(int maxAttempts) {
this.maxAttempts = maxAttempts;
}

public Duration getBackoff() {
return backoff;
}

public void setBackoff(Duration backoff) {
this.backoff = backoff;
}

}

static class NormalizedSource {

private final String name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.StringUtils;

import static org.springframework.cloud.kubernetes.config.PropertySourceUtils.KEY_VALUE_TO_PROPERTIES;
Expand All @@ -43,6 +44,7 @@
* @author Ioannis Canellos
* @author Ali Shahbour
* @author Michael Moudatsos
* @author Roy Jacobs
*/
public class ConfigMapPropertySource extends MapPropertySource {

Expand Down Expand Up @@ -71,7 +73,13 @@ private static Environment createEnvironmentWithActiveProfiles(String[] activePr
}

public ConfigMapPropertySource(KubernetesClient client, String name, String namespace, Environment environment) {
super(getName(client, name, namespace), asObjectMap(getData(client, name, namespace, environment)));
this(client, name, namespace, environment, null);
}

public ConfigMapPropertySource(KubernetesClient client, String name, String namespace, Environment environment,
ConfigMapRetryTemplateFactory configMapRetryTemplateFactory) {
super(getName(client, name, namespace),
asObjectMap(getData(client, name, namespace, environment, configMapRetryTemplateFactory)));
}

private static String getName(KubernetesClient client, String name, String namespace) {
Expand All @@ -81,40 +89,59 @@ private static String getName(KubernetesClient client, String name, String names
}

private static Map<String, Object> getData(KubernetesClient client, String name, String namespace,
Environment environment) {
Environment environment, ConfigMapRetryTemplateFactory configMapRetryTemplateFactory) {
try {
Map<String, Object> result = new LinkedHashMap<>();
ConfigMap map = StringUtils.isEmpty(namespace) ? client.configMaps().withName(name).get()
: client.configMaps().inNamespace(namespace).withName(name).get();

if (map != null) {
result.putAll(processAllEntries(map.getData(), environment));
if (configMapRetryTemplateFactory == null) {
return tryGetData(client, name, namespace, environment);
}
else {
final RetryTemplate retryTemplate = configMapRetryTemplateFactory.getRetryTemplate();
return retryTemplate.execute(ctx -> {
try {
return tryGetData(client, name, namespace, environment);
}
catch (Exception e) {
LOG.warn("Can't read configMap with name: [" + name + "] in namespace:[" + namespace
+ "]. Retrying.", e);
throw e;
}
});
}
}
catch (Exception e) {
LOG.warn("Can't read configMap with name: [" + name + "] in namespace:[" + namespace + "]. Ignoring.", e);
}

return new LinkedHashMap<>();
}

if (environment != null) {
for (String activeProfile : environment.getActiveProfiles()) {
private static Map<String, Object> tryGetData(KubernetesClient client, String name, String namespace,
Environment environment) {
Map<String, Object> result = new LinkedHashMap<>();
ConfigMap map = StringUtils.isEmpty(namespace) ? client.configMaps().withName(name).get()
: client.configMaps().inNamespace(namespace).withName(name).get();

String mapNameWithProfile = name + "-" + activeProfile;
if (map != null) {
result.putAll(processAllEntries(map.getData(), environment));
}

ConfigMap mapWithProfile = StringUtils.isEmpty(namespace)
? client.configMaps().withName(mapNameWithProfile).get()
: client.configMaps().inNamespace(namespace).withName(mapNameWithProfile).get();
if (environment != null) {
for (String activeProfile : environment.getActiveProfiles()) {

if (mapWithProfile != null) {
result.putAll(processAllEntries(mapWithProfile.getData(), environment));
}
String mapNameWithProfile = name + "-" + activeProfile;

}
}
ConfigMap mapWithProfile = StringUtils.isEmpty(namespace)
? client.configMaps().withName(mapNameWithProfile).get()
: client.configMaps().inNamespace(namespace).withName(mapNameWithProfile).get();

return result;
if (mapWithProfile != null) {
result.putAll(processAllEntries(mapWithProfile.getData(), environment));
}

}
catch (Exception e) {
LOG.warn("Can't read configMap with name: [" + name + "] in namespace:[" + namespace + "]. Ignoring.", e);
}
}

return new LinkedHashMap<>();
return result;
}

private static Map<String, Object> processAllEntries(Map<String, String> input, Environment environment) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
*
* @author Ioannis Canellos
* @author Michael Moudatsos
* @author Roy Jacobs
*/
@Order(0)
public class ConfigMapPropertySourceLocator implements PropertySourceLocator {
Expand All @@ -58,9 +59,17 @@ public class ConfigMapPropertySourceLocator implements PropertySourceLocator {

private final ConfigMapConfigProperties properties;

private final ConfigMapRetryTemplateFactory configMapRetryTemplateFactory;

public ConfigMapPropertySourceLocator(KubernetesClient client, ConfigMapConfigProperties properties) {
this(client, properties, null);
}

public ConfigMapPropertySourceLocator(KubernetesClient client, ConfigMapConfigProperties properties,
ConfigMapRetryTemplateFactory configMapRetryTemplateFactory) {
this.client = client;
this.properties = properties;
this.configMapRetryTemplateFactory = configMapRetryTemplateFactory;
}

@Override
Expand All @@ -87,8 +96,8 @@ private MapPropertySource getMapPropertySourceForSingleConfigMap(ConfigurableEnv
String configurationTarget = this.properties.getConfigurationTarget();
return new ConfigMapPropertySource(this.client,
getApplicationName(environment, normalizedSource.getName(), configurationTarget),
getApplicationNamespace(this.client, normalizedSource.getNamespace(), configurationTarget),
environment);
getApplicationNamespace(this.client, normalizedSource.getNamespace(), configurationTarget), environment,
configMapRetryTemplateFactory);
}

private void addPropertySourcesFromPaths(Environment environment, CompositePropertySource composite) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2013-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.kubernetes.config;

import org.springframework.retry.support.RetryTemplate;

/**
* This interface determines which retry logic to apply if there are any errors during the
* loading of config maps at application startup.
*
* @author Roy Jacobs
*/
public interface ConfigMapRetryTemplateFactory {

/**
* @return the {@link RetryTemplate} to use when loading config maps.
*/
RetryTemplate getRetryTemplate();

}
Original file line number Diff line number Diff line change
Expand Up @@ -39,26 +39,21 @@ private ConfigUtils() {
throw new IllegalStateException("Can't instantiate a utility class");
}

public static String getApplicationName(Environment env, String configName,
String configurationTarget) {
public static String getApplicationName(Environment env, String configName, String configurationTarget) {
if (StringUtils.isEmpty(configName)) {
// TODO: use relaxed binding
LOG.debug(configurationTarget
+ " name has not been set, taking it from property/env "
+ SPRING_APPLICATION_NAME + " (default=" + FALLBACK_APPLICATION_NAME
+ ")");
configName = env.getProperty(SPRING_APPLICATION_NAME,
FALLBACK_APPLICATION_NAME);
LOG.debug(configurationTarget + " name has not been set, taking it from property/env "
+ SPRING_APPLICATION_NAME + " (default=" + FALLBACK_APPLICATION_NAME + ")");
configName = env.getProperty(SPRING_APPLICATION_NAME, FALLBACK_APPLICATION_NAME);
}

return configName;
}

public static String getApplicationNamespace(KubernetesClient client,
String configNamespace, String configurationTarget) {
public static String getApplicationNamespace(KubernetesClient client, String configNamespace,
String configurationTarget) {
if (StringUtils.isEmpty(configNamespace)) {
LOG.debug(configurationTarget
+ " namespace has not been set, taking it from client (ns="
LOG.debug(configurationTarget + " namespace has not been set, taking it from client (ns="
+ client.getNamespace() + ")");
configNamespace = client.getNamespace();
}
Expand Down
Loading