Major refactor of configmaps and secrets implementation (#917)

This commit is contained in:
erabii
2022-03-15 02:12:05 +02:00
committed by GitHub
parent efcb0f9c08
commit 79fd04cf00
75 changed files with 3577 additions and 617 deletions

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2013-2022 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.client.config;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSource;
import org.springframework.core.env.Environment;
/**
* A context/holder for various data needed to compute property sources.
*
* @author wind57
*/
public final record KubernetesClientConfigContext(CoreV1Api client, NormalizedSource normalizedSource, String namespace,
Environment environment) {
}

View File

@@ -16,23 +16,12 @@
package org.springframework.cloud.kubernetes.client.config;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.util.EnumMap;
import java.util.Optional;
import org.springframework.cloud.kubernetes.commons.config.ConfigMapPropertySource;
import org.springframework.core.env.Environment;
import org.springframework.util.CollectionUtils;
import static org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigUtils.getApplicationNamespace;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSourceType;
import org.springframework.cloud.kubernetes.commons.config.SourceData;
/**
* @author Ryan Baxter
@@ -40,50 +29,31 @@ import static org.springframework.cloud.kubernetes.client.config.KubernetesClien
*/
public class KubernetesClientConfigMapPropertySource extends ConfigMapPropertySource {
private static final Log LOG = LogFactory.getLog(KubernetesClientConfigMapPropertySource.class);
private static final EnumMap<NormalizedSourceType, KubernetesClientContextToSourceData> STRATEGIES = new EnumMap<>(
NormalizedSourceType.class);
public KubernetesClientConfigMapPropertySource(CoreV1Api coreV1Api, String name, String namespace,
Environment environment, String prefix, boolean includeProfileSpecificSources, boolean failFast) {
super(getName(name, getApplicationNamespace(namespace, "Config Map", null)),
getData(coreV1Api, name, getApplicationNamespace(namespace, "Config Map", null), environment, prefix,
includeProfileSpecificSources, failFast));
// there is a single strategy here at the moment (unlike secrets),
// but this can change.
// to be on par with secrets implementation, I am keeping it the same
static {
STRATEGIES.put(NormalizedSourceType.NAMED_CONFIG_MAP, namedConfigMap());
}
private static Map<String, Object> getData(CoreV1Api coreV1Api, String name, String namespace,
Environment environment, String prefix, boolean includeProfileSpecificSources, boolean failFast) {
public KubernetesClientConfigMapPropertySource(KubernetesClientConfigContext context) {
super(getSourceData(context));
}
LOG.debug("Loading ConfigMap with name '" + name + "' in namespace '" + namespace + "'");
try {
Set<String> names = new HashSet<>();
names.add(name);
if (environment != null && includeProfileSpecificSources) {
for (String activeProfile : environment.getActiveProfiles()) {
names.add(name + "-" + activeProfile);
}
}
Map<String, Object> result = new HashMap<>();
coreV1Api.listNamespacedConfigMap(namespace, null, null, null, null, null, null, null, null, null, null)
.getItems().stream().filter(cm -> names.contains(cm.getMetadata().getName()))
.map(map -> processAllEntries(map.getData(), environment)).collect(Collectors.toList())
.forEach(result::putAll);
private static SourceData getSourceData(KubernetesClientConfigContext context) {
NormalizedSourceType type = context.normalizedSource().type();
return Optional.ofNullable(STRATEGIES.get(type)).map(x -> x.apply(context))
.orElseThrow(() -> new IllegalArgumentException("no strategy found for : " + type));
}
if (!"".equals(prefix)) {
Map<String, Object> withPrefix = CollectionUtils.newHashMap(result.size());
result.forEach((key, value) -> withPrefix.put(prefix + "." + key, value));
return withPrefix;
}
return result;
}
catch (ApiException e) {
if (failFast) {
throw new IllegalStateException(
"Unable to read ConfigMap with name '" + name + "' in namespace '" + namespace + "'", e);
}
LOG.warn("Unable to get ConfigMap " + name + " in namespace " + namespace, e);
}
return Collections.emptyMap();
// we need to pass various functions because the code we are interested in
// is protected in ConfigMapPropertySource, and must stay that way.
private static KubernetesClientContextToSourceData namedConfigMap() {
return NamedConfigMapContextToSourceDataProvider.of(ConfigMapPropertySource::processAllEntries,
ConfigMapPropertySource::getSourceName, ConfigMapPropertySource::withPrefix).get();
}
}

View File

@@ -21,9 +21,11 @@ import io.kubernetes.client.openapi.apis.CoreV1Api;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties;
import org.springframework.cloud.kubernetes.commons.config.ConfigMapPropertySourceLocator;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.util.StringUtils;
import static org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigUtils.getApplicationNamespace;
/**
* @author Ryan Baxter
@@ -43,24 +45,14 @@ public class KubernetesClientConfigMapPropertySourceLocator extends ConfigMapPro
}
@Override
protected MapPropertySource getMapPropertySource(String name,
ConfigMapConfigProperties.NormalizedSource normalizedSource, String configurationTarget,
ConfigurableEnvironment environment) {
protected MapPropertySource getMapPropertySource(NormalizedSource source, ConfigurableEnvironment environment) {
String namespace;
String normalizedNamespace = normalizedSource.getNamespace();
String normalizedNamespace = source.namespace().orElse(null);
String namespace = getApplicationNamespace(normalizedNamespace, source.target(), kubernetesNamespaceProvider);
if (StringUtils.hasText(normalizedNamespace)) {
namespace = normalizedNamespace;
}
else {
namespace = KubernetesClientConfigUtils.getApplicationNamespace(normalizedNamespace, "Config Map",
kubernetesNamespaceProvider);
}
return new KubernetesClientConfigMapPropertySource(coreV1Api, name, namespace, environment,
normalizedSource.getPrefix(), normalizedSource.isIncludeProfileSpecificSources(),
this.properties.isFailFast());
KubernetesClientConfigContext context = new KubernetesClientConfigContext(coreV1Api, source, namespace,
environment);
return new KubernetesClientConfigMapPropertySource(context);
}
}

View File

@@ -16,11 +16,17 @@
package org.springframework.cloud.kubernetes.client.config;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import io.kubernetes.client.openapi.models.V1Secret;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.config.NamespaceResolutionFailedException;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
@@ -71,4 +77,24 @@ public final class KubernetesClientConfigUtils {
throw new NamespaceResolutionFailedException("unresolved namespace");
}
/**
* return decoded data from a secret within a namespace.
*/
static Map<String, Object> dataFromSecret(V1Secret secret, String namespace) {
LOG.debug("reading secret with name : " + secret.getMetadata().getName() + " in namespace : " + namespace);
Map<String, byte[]> data = secret.getData();
Map<String, Object> result = new HashMap<>(CollectionUtils.newHashMap(data.size()));
data.forEach((k, v) -> {
String decodedValue = decoded(v);
result.put(k, decodedValue);
});
return result;
}
private static String decoded(byte[] value) {
return new String(Base64.getDecoder().decode(Base64.getEncoder().encodeToString(value))).trim();
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2013-2022 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.client.config;
import java.util.function.Function;
import org.springframework.cloud.kubernetes.commons.config.SourceData;
/**
* A more succinct way to define a Function from KubernetesClientConfigContext to
* SourceData.
*
* @author wind57
*/
interface KubernetesClientContextToSourceData extends Function<KubernetesClientConfigContext, SourceData> {
}

View File

@@ -16,21 +16,12 @@
package org.springframework.cloud.kubernetes.client.config;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.EnumMap;
import java.util.Optional;
import java.util.stream.Collectors;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.openapi.models.V1Secret;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSourceType;
import org.springframework.cloud.kubernetes.commons.config.SecretsPropertySource;
import org.springframework.util.StringUtils;
import static org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigUtils.getApplicationNamespace;
import org.springframework.cloud.kubernetes.commons.config.SourceData;
/**
* @author Ryan Baxter
@@ -38,59 +29,34 @@ import static org.springframework.cloud.kubernetes.client.config.KubernetesClien
*/
public class KubernetesClientSecretsPropertySource extends SecretsPropertySource {
private static final Log LOG = LogFactory.getLog(KubernetesClientSecretsPropertySource.class);
public KubernetesClientSecretsPropertySource(CoreV1Api coreV1Api, String name, String namespace,
Map<String, String> labels, boolean failFast) {
super(getSourceName(name, getApplicationNamespace(namespace, "Secret", null)),
getSourceData(coreV1Api, name, getApplicationNamespace(namespace, "Secret", null), labels, failFast));
public KubernetesClientSecretsPropertySource(SourceData sourceData) {
super(sourceData);
}
private static Map<String, Object> getSourceData(CoreV1Api api, String name, String namespace,
Map<String, String> labels, boolean failFast) {
Map<String, Object> result = new HashMap<>();
private static final EnumMap<NormalizedSourceType, KubernetesClientContextToSourceData> STRATEGIES = new EnumMap<>(
NormalizedSourceType.class);
LOG.debug("Loading Secret with name '" + name + "' or with labels [" + labels + "] in namespace '" + namespace
+ "'");
try {
if (StringUtils.hasText(name)) {
Optional<V1Secret> secret;
secret = api.listNamespacedSecret(namespace, null, null, null, null, null, null, null, null, null, null)
.getItems().stream().filter(s -> name.equals(s.getMetadata().getName())).findFirst();
secret.ifPresent(s -> putAll(s, result));
}
// Read for secrets api (label)
if (labels != null && !labels.isEmpty()) {
api.listNamespacedSecret(namespace, null, null, null, null, createLabelsSelector(labels), null, null,
null, null, null).getItems().forEach(s -> putAll(s, result));
}
}
catch (Exception e) {
if (failFast) {
throw new IllegalStateException("Unable to read Secret with name '" + name + "' or labels [" + labels
+ "] in namespace '" + namespace + "'", e);
}
LOG.warn("Can't read secret with name: [" + name + "] or labels [" + labels + "] in namespace:[" + namespace
+ "] (cause: " + e.getMessage() + "). Ignoring", e);
}
return result;
static {
STRATEGIES.put(NormalizedSourceType.NAMED_SECRET, namedSecret());
STRATEGIES.put(NormalizedSourceType.LABELED_SECRET, labeledSecret());
}
private static String createLabelsSelector(Map<String, String> labels) {
return labels.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining(","));
public KubernetesClientSecretsPropertySource(KubernetesClientConfigContext context) {
super(getSourceData(context));
}
private static void putAll(V1Secret secret, Map<String, Object> result) {
Map<String, String> secretData = new HashMap<>();
if (secret.getData() != null) {
secret.getData().forEach((key, value) -> secretData.put(key, Base64.getEncoder().encodeToString(value)));
putAll(secretData, result);
}
private static SourceData getSourceData(KubernetesClientConfigContext context) {
NormalizedSourceType type = context.normalizedSource().type();
return Optional.ofNullable(STRATEGIES.get(type)).map(x -> x.apply(context))
.orElseThrow(() -> new IllegalArgumentException("no strategy found for : " + type));
}
private static KubernetesClientContextToSourceData namedSecret() {
return NamedSecretContextToSourceDataProvider.of(SecretsPropertySource::getSourceName).get();
}
private static KubernetesClientContextToSourceData labeledSecret() {
return LabeledSecretContextToSourceDataProvider.of(SecretsPropertySource::getSourceName).get();
}
}

View File

@@ -19,13 +19,13 @@ package org.springframework.cloud.kubernetes.client.config;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties;
import org.springframework.cloud.kubernetes.commons.config.SecretsPropertySourceLocator;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.util.StringUtils;
import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.getApplicationName;
import static org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigUtils.getApplicationNamespace;
/**
* @author Ryan Baxter
@@ -45,23 +45,15 @@ public class KubernetesClientSecretsPropertySourceLocator extends SecretsPropert
}
@Override
protected MapPropertySource getPropertySource(ConfigurableEnvironment environment,
SecretsConfigProperties.NormalizedSource normalizedSource, String configurationTarget) {
protected MapPropertySource getPropertySource(ConfigurableEnvironment environment, NormalizedSource source) {
String namespace;
String normalizedNamespace = normalizedSource.getNamespace();
String secretName = getApplicationName(environment, normalizedSource.getName(), configurationTarget);
String normalizedNamespace = source.namespace().orElse(null);
String namespace = getApplicationNamespace(normalizedNamespace, source.target(), kubernetesNamespaceProvider);
if (StringUtils.hasText(normalizedNamespace)) {
namespace = normalizedNamespace;
}
else {
namespace = KubernetesClientConfigUtils.getApplicationNamespace(normalizedNamespace, "Secret",
kubernetesNamespaceProvider);
}
KubernetesClientConfigContext context = new KubernetesClientConfigContext(coreV1Api, source, namespace,
environment);
return new KubernetesClientSecretsPropertySource(coreV1Api, secretName, namespace, normalizedSource.getLabels(),
this.properties.isFailFast());
return new KubernetesClientSecretsPropertySource(context);
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2013-2022 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.client.config;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.openapi.models.V1Secret;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.kubernetes.commons.config.LabeledSecretNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.SourceData;
import static org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigUtils.dataFromSecret;
import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.onException;
import static org.springframework.cloud.kubernetes.commons.config.Constants.PROPERTY_SOURCE_NAME_SEPARATOR;
/**
* Provides an implementation of {@link KubernetesClientContextToSourceData} for a labeled
* secret.
*
* @author wind57
*/
final class LabeledSecretContextToSourceDataProvider implements Supplier<KubernetesClientContextToSourceData> {
private static final Log LOG = LogFactory.getLog(LabeledSecretContextToSourceDataProvider.class);
private final BiFunction<String, String, String> sourceNameMapper;
private LabeledSecretContextToSourceDataProvider(BiFunction<String, String, String> sourceNameFunction) {
this.sourceNameMapper = Objects.requireNonNull(sourceNameFunction);
}
static LabeledSecretContextToSourceDataProvider of(BiFunction<String, String, String> sourceNameFunction) {
return new LabeledSecretContextToSourceDataProvider(sourceNameFunction);
}
/*
* Computes a ContextSourceData (think content) for secret(s) based on some labels.
* There could be many secrets that are read based on incoming labels, for which we
* will be computing a single Map<String, Object> in the end.
*
* If there is no secret found for the provided labels, we will return an "empty"
* SourceData. Its name is going to be the concatenated labels mapped to an empty Map.
*
* If we find secret(s) for the provided labels, its name is going to be the
* concatenated secret names mapped to the data they hold as a Map.
*/
@Override
public KubernetesClientContextToSourceData get() {
return context -> {
Map<String, Object> result = new HashMap<>();
LabeledSecretNormalizedSource source = (LabeledSecretNormalizedSource) context.normalizedSource();
Map<String, String> labels = source.labels();
String namespace = context.namespace();
String sourceName = String.join(PROPERTY_SOURCE_NAME_SEPARATOR, labels.keySet());
try {
LOG.info("Loading Secret with labels '" + labels + "' in namespace '" + namespace + "'");
List<V1Secret> secrets = context.client().listNamespacedSecret(namespace, null, null, null, null,
createLabelsSelector(labels), null, null, null, null, null).getItems();
if (!secrets.isEmpty()) {
sourceName = secrets.stream().map(V1Secret::getMetadata).map(V1ObjectMeta::getName)
.collect(Collectors.joining(PROPERTY_SOURCE_NAME_SEPARATOR));
secrets.forEach(s -> result.putAll(dataFromSecret(s, namespace)));
}
}
catch (Exception e) {
String message = "Unable to read Secret with labels [" + labels + "] in namespace '" + namespace + "'";
onException(source.failFast(), message, e);
}
String propertySourceName = sourceNameMapper.apply(sourceName, namespace);
return new SourceData(propertySourceName, result);
};
}
private static String createLabelsSelector(Map<String, String> labels) {
return labels.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining(","));
}
}

View File

@@ -0,0 +1,144 @@
/*
* Copyright 2013-2022 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.client.config;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import io.kubernetes.client.openapi.ApiException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.kubernetes.commons.config.ConfigMapPrefixContext;
import org.springframework.cloud.kubernetes.commons.config.NamedConfigMapNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.SourceData;
import org.springframework.core.env.Environment;
import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.getApplicationName;
import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.onException;
import static org.springframework.cloud.kubernetes.commons.config.Constants.PROPERTY_SOURCE_NAME_SEPARATOR;
/**
* Provides an implementation of {@link KubernetesClientContextToSourceData} for a named
* config map.
*
* @author wind57
*/
final class NamedConfigMapContextToSourceDataProvider implements Supplier<KubernetesClientContextToSourceData> {
private static final Log LOG = LogFactory.getLog(NamedConfigMapContextToSourceDataProvider.class);
private final BiFunction<Map<String, String>, Environment, Map<String, Object>> entriesProcessor;
private final BiFunction<String, String, String> sourceNameMapper;
private final Function<ConfigMapPrefixContext, SourceData> withPrefix;
private NamedConfigMapContextToSourceDataProvider(
BiFunction<Map<String, String>, Environment, Map<String, Object>> entriesProcessor,
BiFunction<String, String, String> sourceNameMapper,
Function<ConfigMapPrefixContext, SourceData> withPrefix) {
this.entriesProcessor = Objects.requireNonNull(entriesProcessor);
this.sourceNameMapper = Objects.requireNonNull(sourceNameMapper);
this.withPrefix = Objects.requireNonNull(withPrefix);
}
static NamedConfigMapContextToSourceDataProvider of(
BiFunction<Map<String, String>, Environment, Map<String, Object>> entriesProcessor,
BiFunction<String, String, String> sourceNameMapper,
Function<ConfigMapPrefixContext, SourceData> withPrefix) {
return new NamedConfigMapContextToSourceDataProvider(entriesProcessor, sourceNameMapper, withPrefix);
}
@Override
public KubernetesClientContextToSourceData get() {
return context -> {
NamedConfigMapNormalizedSource source = (NamedConfigMapNormalizedSource) context.normalizedSource();
// namespace has to be read from context, not from the normalized source
String namespace = context.namespace();
Environment environment = context.environment();
String configMapName = appName(environment, source).get();
Set<String> propertySourceNames = new LinkedHashSet<>();
propertySourceNames.add(configMapName);
Map<String, Object> result = new HashMap<>();
try {
Set<String> names = new HashSet<>();
names.add(configMapName);
if (environment != null && source.profileSpecificSources()) {
for (String activeProfile : environment.getActiveProfiles()) {
names.add(configMapName + "-" + activeProfile);
}
}
/*
* 1. read all config maps in the namespace 2. filter the ones that match
* user supplied name + profile specific ones 3. get an Entry that
* contains the data from config map + its name 4. create a single map
* that contains all the data from all config maps; also create a unified
* name of the property source (combined config map names).
*/
context.client()
.listNamespacedConfigMap(namespace, null, null, null, null, null, null, null, null, null, null)
.getItems().stream().filter(cm -> names.contains(cm.getMetadata().getName()))
.map(cm -> new AbstractMap.SimpleEntry<>(entriesProcessor.apply(cm.getData(), environment),
cm.getMetadata().getName()))
.collect(Collectors.toList()).forEach(entry -> {
LOG.info("Loaded config map with name : '" + entry.getValue() + "' in namespace : '"
+ namespace + "'");
result.putAll(entry.getKey());
propertySourceNames.add(entry.getValue());
});
if (!"".equals(source.prefix())) {
ConfigMapPrefixContext prefixContext = new ConfigMapPrefixContext(result, source.prefix(),
namespace, propertySourceNames);
return withPrefix.apply(prefixContext);
}
}
catch (ApiException e) {
// we could make the error tell exactly what config map we could not read,
// this would mean separate calls to kubeapi server via the client,
// though.
String message = "Unable to read ConfigMap(s) in namespace '" + namespace + "'";
onException(source.failFast(), message, e);
}
String propertySourceTokens = String.join(PROPERTY_SOURCE_NAME_SEPARATOR, propertySourceNames);
return new SourceData(sourceNameMapper.apply(propertySourceTokens, namespace), result);
};
}
private Supplier<String> appName(Environment environment, NormalizedSource normalizedSource) {
return () -> getApplicationName(environment, normalizedSource.name().orElse(null), normalizedSource.target());
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2013-2022 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.client.config;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import io.kubernetes.client.openapi.models.V1Secret;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.kubernetes.commons.config.NamedSecretNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.SourceData;
import static org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigUtils.dataFromSecret;
import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.onException;
/**
* Provides an implementation of {@link KubernetesClientContextToSourceData} for a named
* secret.
*
* @author wind57
*/
final class NamedSecretContextToSourceDataProvider implements Supplier<KubernetesClientContextToSourceData> {
private static final Log LOG = LogFactory.getLog(NamedSecretContextToSourceDataProvider.class);
private final BiFunction<String, String, String> sourceNameMapper;
private NamedSecretContextToSourceDataProvider(BiFunction<String, String, String> sourceNameFunction) {
this.sourceNameMapper = Objects.requireNonNull(sourceNameFunction);
}
static NamedSecretContextToSourceDataProvider of(BiFunction<String, String, String> sourceNameFunction) {
return new NamedSecretContextToSourceDataProvider(sourceNameFunction);
}
@Override
public KubernetesClientContextToSourceData get() {
return context -> {
NamedSecretNormalizedSource source = (NamedSecretNormalizedSource) context.normalizedSource();
Map<String, Object> result = new HashMap<>();
String namespace = context.namespace();
// error should never be thrown here, since we always expect a name
// explicit or implicit
String name = source.name().orElseThrow();
try {
LOG.info("Loading Secret with name '" + name + "' in namespace '" + namespace + "'");
Optional<V1Secret> secret;
secret = context.client()
.listNamespacedSecret(namespace, null, null, null, null, null, null, null, null, null, null)
.getItems().stream().filter(s -> name.equals(s.getMetadata().getName())).findFirst();
secret.ifPresent(s -> result.putAll(dataFromSecret(s, namespace)));
}
catch (Exception e) {
String message = "Unable to read Secret with name '" + name + "' in namespace '" + namespace + "'";
onException(source.failFast(), message, e);
}
String propertySourceName = sourceNameMapper.apply(name, namespace);
return new SourceData(propertySourceName, result);
};
}
}

View File

@@ -183,7 +183,7 @@ class KubernetesClientConfigMapPropertySourceLocatorTests {
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
assertThatThrownBy(() -> locator.locate(new MockEnvironment())).isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read ConfigMap with name 'bootstrap-640' in namespace 'default'");
.hasMessage("Unable to read ConfigMap(s) in namespace 'default'");
}
@Test

View File

@@ -31,7 +31,8 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.kubernetes.commons.config.NamespaceResolutionFailedException;
import org.springframework.cloud.kubernetes.commons.config.NamedConfigMapNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSource;
import org.springframework.mock.env.MockEnvironment;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
@@ -98,8 +99,12 @@ class KubernetesClientConfigMapPropertySourceTests {
CoreV1Api api = new CoreV1Api();
stubFor(get("/api/v1/namespaces/default/configmaps")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(PROPERTIES_CONFIGMAP_LIST))));
KubernetesClientConfigMapPropertySource propertySource = new KubernetesClientConfigMapPropertySource(api,
"bootstrap-640", "default", new MockEnvironment(), "", true, false);
NormalizedSource source = new NamedConfigMapNormalizedSource("bootstrap-640", "default", false, "", true);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, "default",
new MockEnvironment());
KubernetesClientConfigMapPropertySource propertySource = new KubernetesClientConfigMapPropertySource(context);
verify(getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps")));
assertThat(propertySource.containsProperty("spring.cloud.kubernetes.configuration.watcher.refreshDelay"))
.isTrue();
@@ -115,8 +120,12 @@ class KubernetesClientConfigMapPropertySourceTests {
CoreV1Api api = new CoreV1Api();
stubFor(get("/api/v1/namespaces/default/configmaps")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(YAML_CONFIGMAP_LIST))));
KubernetesClientConfigMapPropertySource propertySource = new KubernetesClientConfigMapPropertySource(api,
"bootstrap-641", "default", new MockEnvironment(), "", true, false);
NormalizedSource source = new NamedConfigMapNormalizedSource("bootstrap-641", "default", false, "", true);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, "default",
new MockEnvironment());
KubernetesClientConfigMapPropertySource propertySource = new KubernetesClientConfigMapPropertySource(context);
verify(getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps")));
assertThat(propertySource.containsProperty("dummy.property.string2")).isTrue();
assertThat(propertySource.getProperty("dummy.property.string2")).isEqualTo("a");
@@ -132,8 +141,12 @@ class KubernetesClientConfigMapPropertySourceTests {
CoreV1Api api = new CoreV1Api();
stubFor(get("/api/v1/namespaces/default/configmaps")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(PROPERTIES_CONFIGMAP_LIST))));
KubernetesClientConfigMapPropertySource propertySource = new KubernetesClientConfigMapPropertySource(api,
"bootstrap-640", "default", new MockEnvironment(), "prefix", true, false);
NormalizedSource source = new NamedConfigMapNormalizedSource("bootstrap-640", "default", false, "prefix", true);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, "default",
new MockEnvironment());
KubernetesClientConfigMapPropertySource propertySource = new KubernetesClientConfigMapPropertySource(context);
verify(getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps")));
assertThat(propertySource.containsProperty("prefix.spring.cloud.kubernetes.configuration.watcher.refreshDelay"))
.isTrue();
@@ -145,38 +158,41 @@ class KubernetesClientConfigMapPropertySourceTests {
.isEqualTo("TRACE");
}
@Test
void constructorWithoutNamespaceMustFail() {
assertThatThrownBy(() -> new KubernetesClientConfigMapPropertySource(new CoreV1Api(), "configmap", null,
new MockEnvironment(), "", false, false)).isInstanceOf(NamespaceResolutionFailedException.class);
}
@Test
void constructorWithNamespaceMustNotFail() {
assertThat(new KubernetesClientConfigMapPropertySource(new CoreV1Api(), "configmap", "namespace",
new MockEnvironment(), "", false, false)).isNotNull();
NormalizedSource source = new NamedConfigMapNormalizedSource("bootstrap-640", "default", false, "prefix", true);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(new CoreV1Api(), source, "default",
new MockEnvironment());
assertThat(new KubernetesClientConfigMapPropertySource(context)).isNotNull();
}
@Test
public void constructorShouldThrowExceptionOnFailureWhenFailFastIsEnabled() {
CoreV1Api api = new CoreV1Api();
stubFor(get("/api/v1/namespaces/default/configmaps")
.willReturn(aResponse().withStatus(500).withBody("Internal Server Error")));
assertThatThrownBy(() -> new KubernetesClientConfigMapPropertySource(api, "my-config", "default",
new MockEnvironment(), "", false, true)).isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read ConfigMap with name 'my-config' in namespace 'default'");
NormalizedSource source = new NamedConfigMapNormalizedSource("my-config", "default", true, "prefix", true);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(new CoreV1Api(), source, "default",
new MockEnvironment());
assertThatThrownBy(() -> new KubernetesClientConfigMapPropertySource(context))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read ConfigMap(s) in namespace 'default'");
verify(getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps")));
}
@Test
public void constructorShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() {
CoreV1Api api = new CoreV1Api();
stubFor(get("/api/v1/namespaces/default/configmaps")
.willReturn(aResponse().withStatus(500).withBody("Internal Server Error")));
assertThatNoException().isThrownBy((() -> new KubernetesClientConfigMapPropertySource(api, "my-config",
"default", new MockEnvironment(), "", false, false)));
NormalizedSource source = new NamedConfigMapNormalizedSource("my-config", "default", false, "prefix", true);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(new CoreV1Api(), source, "default",
new MockEnvironment());
assertThatNoException().isThrownBy((() -> new KubernetesClientConfigMapPropertySource(context)));
verify(getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps")));
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.kubernetes.client.config.reload;
package org.springframework.cloud.kubernetes.client.config;
import java.util.ArrayList;
import java.util.Arrays;
@@ -46,7 +46,7 @@ import org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAu
import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration;
import org.springframework.cloud.autoconfigure.RefreshEndpointAutoConfiguration;
import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration;
import org.springframework.cloud.kubernetes.client.config.KubernetesClientBootstrapConfiguration;
import org.springframework.cloud.kubernetes.client.config.reload.KubernetesClientConfigReloadAutoConfiguration;
import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties;
import org.springframework.cloud.kubernetes.commons.config.KubernetesBootstrapConfiguration;
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadAutoConfiguration;

View File

@@ -174,7 +174,7 @@ class KubernetesClientSecretsPropertySourceLocatorTests {
new KubernetesNamespaceProvider(new MockEnvironment()), secretsConfigProperties);
assertThatThrownBy(() -> locator.locate(new MockEnvironment())).isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read Secret with name 'db-secret' or labels [{}] in namespace 'default'");
.hasMessage("Unable to read Secret with name 'db-secret' in namespace 'default'");
}
@Test

View File

@@ -35,7 +35,10 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.kubernetes.commons.config.NamespaceResolutionFailedException;
import org.springframework.cloud.kubernetes.commons.config.LabeledSecretNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.NamedSecretNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSource;
import org.springframework.mock.env.MockEnvironment;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
@@ -115,8 +118,12 @@ class KubernetesClientSecretsPropertySourceTests {
void secretsTest() {
CoreV1Api api = new CoreV1Api();
stubFor(get(API).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(SECRET_LIST))));
KubernetesClientSecretsPropertySource propertySource = new KubernetesClientSecretsPropertySource(api,
"db-secret", "default", new HashMap<>(), false);
NormalizedSource source = new NamedSecretNormalizedSource("db-secret", "default", false);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, "default",
new MockEnvironment());
KubernetesClientSecretsPropertySource propertySource = new KubernetesClientSecretsPropertySource(context);
assertThat(propertySource.containsProperty("password")).isTrue();
assertThat(propertySource.getProperty("password")).isEqualTo("p455w0rd");
assertThat(propertySource.containsProperty("username")).isTrue();
@@ -129,8 +136,12 @@ class KubernetesClientSecretsPropertySourceTests {
stubFor(get(LIST_API_WITH_LABEL).willReturn(aResponse().withStatus(200).withBody(LIST_BODY)));
Map<String, String> labels = new HashMap<>();
labels.put("spring.cloud.kubernetes.secret", "true");
KubernetesClientSecretsPropertySource propertySource = new KubernetesClientSecretsPropertySource(api, null,
"default", labels, false);
NormalizedSource source = new LabeledSecretNormalizedSource("default", labels, false);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, "default",
new MockEnvironment());
KubernetesClientSecretsPropertySource propertySource = new KubernetesClientSecretsPropertySource(context);
assertThat(propertySource.containsProperty("spring.rabbitmq.password")).isTrue();
assertThat(propertySource.getProperty("spring.rabbitmq.password")).isEqualTo("password");
}
@@ -140,9 +151,13 @@ class KubernetesClientSecretsPropertySourceTests {
CoreV1Api api = new CoreV1Api();
stubFor(get(API).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")));
assertThatThrownBy(() -> new KubernetesClientSecretsPropertySource(api, "db-secret", "default", null, true))
NormalizedSource source = new NamedSecretNormalizedSource("secret", "default", true);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, "default",
new MockEnvironment());
assertThatThrownBy(() -> new KubernetesClientSecretsPropertySource(context))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read Secret with name 'db-secret' or labels [null] in namespace 'default'");
.hasMessage("Unable to read Secret with name 'secret' in namespace 'default'");
verify(getRequestedFor(urlEqualTo(API)));
}
@@ -151,16 +166,12 @@ class KubernetesClientSecretsPropertySourceTests {
CoreV1Api api = new CoreV1Api();
stubFor(get(API).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")));
assertThatNoException().isThrownBy(
(() -> new KubernetesClientSecretsPropertySource(api, "db-secret", "default", null, false)));
NormalizedSource source = new NamedSecretNormalizedSource("secret", "db-secret", false);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, "default",
new MockEnvironment());
assertThatNoException().isThrownBy((() -> new KubernetesClientSecretsPropertySource(context)));
verify(getRequestedFor(urlEqualTo(API)));
}
@Test
void constructorMustFailWhenNamespaceIsNoProvided() {
CoreV1Api api = new CoreV1Api();
assertThatThrownBy((() -> new KubernetesClientSecretsPropertySource(api, "db-secret", null, null, false)))
.isInstanceOf(NamespaceResolutionFailedException.class);
}
}

View File

@@ -0,0 +1,214 @@
/*
* Copyright 2013-2022 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.client.config;
import java.util.Base64;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.Configuration;
import io.kubernetes.client.openapi.JSON;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder;
import io.kubernetes.client.openapi.models.V1SecretBuilder;
import io.kubernetes.client.openapi.models.V1SecretList;
import io.kubernetes.client.util.ClientBuilder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.kubernetes.commons.config.LabeledSecretNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.SecretsPropertySource;
import org.springframework.cloud.kubernetes.commons.config.SourceData;
import org.springframework.mock.env.MockEnvironment;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
/**
* @author wind57
*/
class LabeledSecretContextToSourceDataProviderTests {
private static final Map<String, String> LABELS = new LinkedHashMap<>();
private static final Map<String, String> RED_LABEL = Map.of("color", "red");
private static final String NAMESPACE = "default";
static {
LABELS.put("label2", "value2");
LABELS.put("label1", "value1");
}
@BeforeAll
static void setup() {
WireMockServer wireMockServer = new WireMockServer(options().dynamicPort());
wireMockServer.start();
WireMock.configureFor("localhost", wireMockServer.port());
ApiClient client = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build();
client.setDebugging(true);
Configuration.setDefaultApiClient(client);
}
@AfterEach
void afterEach() {
WireMock.reset();
}
/**
* we have a single secret deployed. it does not match our query.
*/
@Test
void noMatch() {
V1SecretList secretList = new V1SecretList().addItemsItem(new V1SecretBuilder()
.withMetadata(new V1ObjectMetaBuilder().withLabels(Collections.singletonMap("color", "red"))
.withNamespace(NAMESPACE).withName("red-secret").withResourceVersion("1").build())
.addToData("color", Base64.getEncoder().encode("really-red".getBytes())).build());
CoreV1Api api = new CoreV1Api();
stubFor(get("/api/v1/namespaces/default/secrets?labelSelector=color%3Dred")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(secretList))));
// blue does not match red
NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE,
Collections.singletonMap("color", "blue"), false);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE,
new MockEnvironment());
KubernetesClientContextToSourceData data = LabeledSecretContextToSourceDataProvider
.of(LabeledSecretContextToSourceDataProviderTests.Dummy::sourceName).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "secrets.color.default");
Assertions.assertEquals(sourceData.sourceData(), Collections.emptyMap());
}
/**
* we have a single secret deployed. it has two labels and these match against our
* queries.
*/
@Test
void singleSecretMatchAgainstLabels() {
V1SecretList SECRETS_LIST = new V1SecretList().addItemsItem(new V1SecretBuilder()
.withMetadata(new V1ObjectMetaBuilder().withLabels(LABELS).withNamespace(NAMESPACE)
.withResourceVersion("1").withName("test-secret").build())
.addToData("color", "really-red".getBytes()).build());
CoreV1Api api = new CoreV1Api();
stubFor(get("/api/v1/namespaces/default/secrets?labelSelector=label2%3Dvalue2%2Clabel1%3Dvalue1")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(SECRETS_LIST))));
NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, LABELS, false);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE,
new MockEnvironment());
KubernetesClientContextToSourceData data = LabeledSecretContextToSourceDataProvider
.of(LabeledSecretContextToSourceDataProviderTests.Dummy::sourceName).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "secrets.test-secret.default");
Assertions.assertEquals(sourceData.sourceData(), Map.of("color", "really-red"));
}
/**
* we have two secrets deployed. both of them have labels that match (color=red).
*/
@Test
void twoSecretsMatchAgainstLabels() {
V1SecretList secretList = new V1SecretList();
secretList.addItemsItem(new V1SecretBuilder()
.withMetadata(new V1ObjectMetaBuilder().withLabels(RED_LABEL).withNamespace(NAMESPACE)
.withResourceVersion("1").withName("color-one").build())
.addToData("colorOne", "really-red-one".getBytes()).build());
secretList.addItemsItem(new V1SecretBuilder()
.withMetadata(new V1ObjectMetaBuilder().withLabels(RED_LABEL).withNamespace(NAMESPACE)
.withResourceVersion("1").withName("color-two").build())
.addToData("colorTwo", "really-red-two".getBytes()).build());
CoreV1Api api = new CoreV1Api();
stubFor(get("/api/v1/namespaces/default/secrets?labelSelector=color%3Dred")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(secretList))));
NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, RED_LABEL, false);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE,
new MockEnvironment());
KubernetesClientContextToSourceData data = LabeledSecretContextToSourceDataProvider
.of(LabeledSecretContextToSourceDataProviderTests.Dummy::sourceName).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "secrets.color-one.color-two.default");
Assertions.assertEquals(sourceData.sourceData().size(), 2);
Assertions.assertEquals(sourceData.sourceData().get("colorOne"), "really-red-one");
Assertions.assertEquals(sourceData.sourceData().get("colorTwo"), "really-red-two");
}
@Test
void namespaceMatch() {
V1SecretList SECRETS_LIST = new V1SecretList().addItemsItem(new V1SecretBuilder()
.withMetadata(new V1ObjectMetaBuilder().withLabels(LABELS).withNamespace(NAMESPACE)
.withResourceVersion("1").withName("test-secret").build())
.addToData("color", "really-red".getBytes()).build());
CoreV1Api api = new CoreV1Api();
stubFor(get("/api/v1/namespaces/default/secrets?labelSelector=label2%3Dvalue2%2Clabel1%3Dvalue1")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(SECRETS_LIST))));
NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE + "nope", LABELS, false);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE,
new MockEnvironment());
KubernetesClientContextToSourceData data = LabeledSecretContextToSourceDataProvider
.of(LabeledSecretContextToSourceDataProviderTests.Dummy::sourceName).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "secrets.test-secret.default");
Assertions.assertEquals(sourceData.sourceData(), Map.of("color", "really-red"));
}
// needed only to allow access to the super methods
private static final class Dummy extends SecretsPropertySource {
private Dummy() {
super(SourceData.emptyRecord("dummy-name"));
}
private static String sourceName(String name, String namespace) {
return getSourceName(name, namespace);
}
}
}

View File

@@ -0,0 +1,340 @@
/*
* Copyright 2013-2022 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.client.config;
import java.util.Collections;
import java.util.Map;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.Configuration;
import io.kubernetes.client.openapi.JSON;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.openapi.models.V1ConfigMapBuilder;
import io.kubernetes.client.openapi.models.V1ConfigMapList;
import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder;
import io.kubernetes.client.util.ClientBuilder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.kubernetes.commons.config.ConfigMapPrefixContext;
import org.springframework.cloud.kubernetes.commons.config.ConfigMapPropertySource;
import org.springframework.cloud.kubernetes.commons.config.NamedConfigMapNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.SourceData;
import org.springframework.core.env.Environment;
import org.springframework.mock.env.MockEnvironment;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
/**
* @author wind57
*/
class NamedConfigMapContextToSourceDataProviderTests {
private static final String NAMESPACE = "default";
private static final String RED_CONFIG_MAP_NAME = "red";
private static final String BLUE_CONFIG_MAP_NAME = "blue";
@BeforeAll
static void setup() {
WireMockServer wireMockServer = new WireMockServer(options().dynamicPort());
wireMockServer.start();
WireMock.configureFor("localhost", wireMockServer.port());
ApiClient client = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build();
client.setDebugging(true);
Configuration.setDefaultApiClient(client);
}
@AfterEach
void afterEach() {
WireMock.reset();
}
/**
* we have a single config map deployed. it does not match our query.
*/
@Test
void noMatch() {
V1ConfigMapList configMapList = new V1ConfigMapList()
.addItemsItem(
new V1ConfigMapBuilder()
.withMetadata(new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME)
.withNamespace(NAMESPACE).withResourceVersion("1").build())
.addToData("color", "really-red").build());
CoreV1Api api = new CoreV1Api();
stubFor(get("/api/v1/namespaces/default/configmaps")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(configMapList))));
NormalizedSource source = new NamedConfigMapNormalizedSource(BLUE_CONFIG_MAP_NAME, NAMESPACE, true, "", false);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE,
new MockEnvironment());
KubernetesClientContextToSourceData data = NamedConfigMapContextToSourceDataProvider
.of(Dummy::processEntries, Dummy::sourceName, Dummy::prefix).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "configmap.blue.default");
Assertions.assertEquals(sourceData.sourceData(), Collections.emptyMap());
}
/**
* we have a single config map deployed. it matches our query.
*/
@Test
void match() {
V1ConfigMapList configMapList = new V1ConfigMapList()
.addItemsItem(
new V1ConfigMapBuilder()
.withMetadata(new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME)
.withNamespace(NAMESPACE).withResourceVersion("1").build())
.addToData("color", "really-red").build());
CoreV1Api api = new CoreV1Api();
stubFor(get("/api/v1/namespaces/default/configmaps")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(configMapList))));
NormalizedSource source = new NamedConfigMapNormalizedSource(RED_CONFIG_MAP_NAME, NAMESPACE, true, "", false);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE,
new MockEnvironment());
KubernetesClientContextToSourceData data = NamedConfigMapContextToSourceDataProvider
.of(Dummy::processEntries, Dummy::sourceName, Dummy::prefix).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "configmap.red.default");
Assertions.assertEquals(sourceData.sourceData(), Collections.singletonMap("color", "really-red"));
}
/**
* we have two config maps deployed. one matches the query name. the other matches the
* active profile + name, thus is taken also.
*/
@Test
void matchIncludeSingleProfile() {
V1ConfigMapList configMapList = new V1ConfigMapList()
.addItemsItem(
new V1ConfigMapBuilder()
.withMetadata(
new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME).withNamespace(NAMESPACE)
.withResourceVersion("1").build())
.addToData("color", "really-red").build())
.addItemsItem(new V1ConfigMapBuilder()
.withMetadata(new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME + "-with-profile")
.withNamespace(NAMESPACE).withResourceVersion("1").build())
.addToData("taste", "mango").build());
CoreV1Api api = new CoreV1Api();
stubFor(get("/api/v1/namespaces/default/configmaps")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(configMapList))));
NormalizedSource source = new NamedConfigMapNormalizedSource(RED_CONFIG_MAP_NAME, NAMESPACE, true, "", true);
MockEnvironment environment = new MockEnvironment();
environment.setActiveProfiles("with-profile");
KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment);
KubernetesClientContextToSourceData data = NamedConfigMapContextToSourceDataProvider
.of(Dummy::processEntries, Dummy::sourceName, Dummy::prefix).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "configmap.red.red-with-profile.default");
Assertions.assertEquals(sourceData.sourceData().size(), 2);
Assertions.assertEquals(sourceData.sourceData().get("color"), "really-red");
Assertions.assertEquals(sourceData.sourceData().get("taste"), "mango");
}
/**
* we have two config maps deployed. one matches the query name. the other matches the
* active profile + name, thus is taken also. This takes into consideration the
* prefix, that we explicitly specify. Notice that prefix works for profile based
* config maps as well.
*/
@Test
void matchIncludeSingleProfileWithPrefix() {
V1ConfigMapList configMapList = new V1ConfigMapList()
.addItemsItem(
new V1ConfigMapBuilder()
.withMetadata(
new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME).withNamespace(NAMESPACE)
.withResourceVersion("1").build())
.addToData("color", "really-red").build())
.addItemsItem(new V1ConfigMapBuilder()
.withMetadata(new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME + "-with-profile")
.withNamespace(NAMESPACE).withResourceVersion("1").build())
.addToData("taste", "mango").build());
CoreV1Api api = new CoreV1Api();
stubFor(get("/api/v1/namespaces/default/configmaps")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(configMapList))));
NormalizedSource source = new NamedConfigMapNormalizedSource(RED_CONFIG_MAP_NAME, NAMESPACE, true, "some",
true);
MockEnvironment environment = new MockEnvironment();
environment.setActiveProfiles("with-profile");
KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment);
KubernetesClientContextToSourceData data = NamedConfigMapContextToSourceDataProvider
.of(Dummy::processEntries, Dummy::sourceName, Dummy::prefix).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "configmap.red.red-with-profile.default");
Assertions.assertEquals(sourceData.sourceData().size(), 2);
Assertions.assertEquals(sourceData.sourceData().get("some.color"), "really-red");
Assertions.assertEquals(sourceData.sourceData().get("some.taste"), "mango");
}
/**
* we have three config maps deployed. one matches the query name. the other two match
* the active profile + name, thus are taken also. This takes into consideration the
* prefix, that we explicitly specify. Notice that prefix works for profile based
* config maps as well.
*/
@Test
void matchIncludeTwoProfilesWithPrefix() {
V1ConfigMapList configMapList = new V1ConfigMapList()
.addItemsItem(
new V1ConfigMapBuilder()
.withMetadata(
new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME).withNamespace(NAMESPACE)
.withResourceVersion("1").build())
.addToData("color", "really-red").build())
.addItemsItem(
new V1ConfigMapBuilder()
.withMetadata(
new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME + "-with-taste")
.withNamespace(NAMESPACE).withResourceVersion("1").build())
.addToData("taste", "mango").build())
.addItemsItem(new V1ConfigMapBuilder()
.withMetadata(new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME + "-with-shape")
.withNamespace(NAMESPACE).withResourceVersion("1").build())
.addToData("shape", "round").build());
CoreV1Api api = new CoreV1Api();
stubFor(get("/api/v1/namespaces/default/configmaps")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(configMapList))));
NormalizedSource source = new NamedConfigMapNormalizedSource(RED_CONFIG_MAP_NAME, NAMESPACE, true, "some",
true);
MockEnvironment environment = new MockEnvironment();
environment.setActiveProfiles("with-taste", "with-shape");
KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment);
KubernetesClientContextToSourceData data = NamedConfigMapContextToSourceDataProvider
.of(Dummy::processEntries, Dummy::sourceName, Dummy::prefix).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "configmap.red.red-with-taste.red-with-shape.default");
Assertions.assertEquals(sourceData.sourceData().size(), 3);
Assertions.assertEquals(sourceData.sourceData().get("some.color"), "really-red");
Assertions.assertEquals(sourceData.sourceData().get("some.taste"), "mango");
Assertions.assertEquals(sourceData.sourceData().get("some.shape"), "round");
}
// this test makes sure that even if NormalizedSource has no name (which is a valid
// case for config maps),
// it will default to "application" and such a config map will be read.
@Test
void matchWithoutName() {
V1ConfigMapList configMapList = new V1ConfigMapList()
.addItemsItem(new V1ConfigMapBuilder().withMetadata(new V1ObjectMetaBuilder().withName("application")
.withNamespace(NAMESPACE).withResourceVersion("1").build()).addToData("color", "red").build());
CoreV1Api api = new CoreV1Api();
stubFor(get("/api/v1/namespaces/default/configmaps")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(configMapList))));
NormalizedSource source = new NamedConfigMapNormalizedSource(null, NAMESPACE, true, "some", false);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE,
new MockEnvironment());
KubernetesClientContextToSourceData data = NamedConfigMapContextToSourceDataProvider
.of(Dummy::processEntries, Dummy::sourceName, Dummy::prefix).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "configmap.application.default");
Assertions.assertEquals(sourceData.sourceData(), Collections.singletonMap("some.color", "red"));
}
/**
* NamedSecretContextToSourceDataProvider gets as input a
* KubernetesClientConfigContext. This context has a namespace as well as a
* NormalizedSource, that has a namespace too. It is easy to get confused in code on
* which namespace to use. This test makes sure that we use the proper one.
*/
@Test
void namespaceMatch() {
V1ConfigMapList configMapList = new V1ConfigMapList()
.addItemsItem(
new V1ConfigMapBuilder()
.withMetadata(new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME)
.withNamespace(NAMESPACE).withResourceVersion("1").build())
.addToData("color", "really-red").build());
CoreV1Api api = new CoreV1Api();
stubFor(get("/api/v1/namespaces/default/configmaps")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(configMapList))));
NormalizedSource source = new NamedConfigMapNormalizedSource(RED_CONFIG_MAP_NAME, NAMESPACE + "nope", true, "",
false);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE,
new MockEnvironment());
KubernetesClientContextToSourceData data = NamedConfigMapContextToSourceDataProvider
.of(Dummy::processEntries, Dummy::sourceName, Dummy::prefix).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "configmap.red.default");
Assertions.assertEquals(sourceData.sourceData(), Collections.singletonMap("color", "really-red"));
}
// needed only to allow access to the super methods
private static final class Dummy extends ConfigMapPropertySource {
private Dummy() {
super(SourceData.emptyRecord("dummy-name"));
}
private static String sourceName(String name, String namespace) {
return getSourceName(name, namespace);
}
private static Map<String, Object> processEntries(Map<String, String> map, Environment environment) {
return processAllEntries(map, environment);
}
private static SourceData prefix(ConfigMapPrefixContext context) {
return withPrefix(context);
}
}
}

View File

@@ -0,0 +1,210 @@
/*
* Copyright 2013-2022 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.client.config;
import java.util.Collections;
import java.util.Map;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.Configuration;
import io.kubernetes.client.openapi.JSON;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder;
import io.kubernetes.client.openapi.models.V1SecretBuilder;
import io.kubernetes.client.openapi.models.V1SecretList;
import io.kubernetes.client.util.ClientBuilder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.kubernetes.commons.config.NamedSecretNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.SecretsPropertySource;
import org.springframework.cloud.kubernetes.commons.config.SourceData;
import org.springframework.mock.env.MockEnvironment;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
class NamedSecretContextToSourceDataProviderTests {
private static final String NAMESPACE = "default";
@BeforeAll
static void setup() {
WireMockServer wireMockServer = new WireMockServer(options().dynamicPort());
wireMockServer.start();
WireMock.configureFor("localhost", wireMockServer.port());
ApiClient client = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build();
client.setDebugging(true);
Configuration.setDefaultApiClient(client);
}
@AfterEach
void afterEach() {
WireMock.reset();
}
/**
*
* /** we have a single secret deployed. it matched the name in our queries
*/
@Test
void singleSecretMatchAgainstLabels() {
V1SecretList secretList = new V1SecretList()
.addItemsItem(
new V1SecretBuilder()
.withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("red")
.withResourceVersion("1").build())
.addToData("color", "really-red".getBytes()).build());
CoreV1Api api = new CoreV1Api();
stubFor(get("/api/v1/namespaces/default/secrets")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(secretList))));
// blue does not match red
NormalizedSource source = new NamedSecretNormalizedSource("red", NAMESPACE, false);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE,
new MockEnvironment());
KubernetesClientContextToSourceData data = NamedSecretContextToSourceDataProvider.of(Dummy::sourceName).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "secrets.red.default");
Assertions.assertEquals(sourceData.sourceData(), Map.of("color", "really-red"));
}
/**
* we have three secrets deployed. one of them has a name that matches (red), the
* other two have different names, thus no match.
*/
@Test
void twoSecretMatchAgainstLabels() {
V1SecretList secretList = new V1SecretList()
.addItemsItem(
new V1SecretBuilder()
.withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("red")
.withResourceVersion("1").build())
.addToData("color", "really-red".getBytes()).build())
.addItemsItem(
new V1SecretBuilder()
.withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("blue")
.withResourceVersion("1").build())
.addToData("color", "really-red".getBytes()).build())
.addItemsItem(
new V1SecretBuilder()
.withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("pink")
.withResourceVersion("1").build())
.addToData("color", "really-red".getBytes()).build());
CoreV1Api api = new CoreV1Api();
stubFor(get("/api/v1/namespaces/default/secrets")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(secretList))));
// blue does not match red
NormalizedSource source = new NamedSecretNormalizedSource("red", NAMESPACE, false);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE,
new MockEnvironment());
KubernetesClientContextToSourceData data = NamedSecretContextToSourceDataProvider.of(Dummy::sourceName).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "secrets.red.default");
Assertions.assertEquals(sourceData.sourceData().size(), 1);
Assertions.assertEquals(sourceData.sourceData().get("color"), "really-red");
}
/**
* one secret deployed (pink), does not match our query (blue).
*/
@Test
void testSecretNoMatch() {
V1SecretList secretList = new V1SecretList()
.addItemsItem(
new V1SecretBuilder()
.withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("red")
.withResourceVersion("1").build())
.addToData("color", "really-red".getBytes()).build());
CoreV1Api api = new CoreV1Api();
stubFor(get("/api/v1/namespaces/default/secrets")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(secretList))));
// blue does not match red
NormalizedSource source = new NamedSecretNormalizedSource("blue", NAMESPACE, false);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE,
new MockEnvironment());
KubernetesClientContextToSourceData data = NamedSecretContextToSourceDataProvider.of(Dummy::sourceName).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "secrets.blue.default");
Assertions.assertEquals(sourceData.sourceData(), Collections.emptyMap());
}
@Test
void namespaceMatch() {
V1SecretList secretList = new V1SecretList()
.addItemsItem(
new V1SecretBuilder()
.withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("red")
.withResourceVersion("1").build())
.addToData("color", "really-red".getBytes()).build());
CoreV1Api api = new CoreV1Api();
stubFor(get("/api/v1/namespaces/default/secrets")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(secretList))));
// blue does not match red
NormalizedSource source = new NamedSecretNormalizedSource("red", NAMESPACE + "nope", false);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE,
new MockEnvironment());
KubernetesClientContextToSourceData data = NamedSecretContextToSourceDataProvider.of(Dummy::sourceName).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "secrets.red.default");
Assertions.assertEquals(sourceData.sourceData(), Map.of("color", "really-red"));
}
// needed only to allow access to the super methods
private static final class Dummy extends SecretsPropertySource {
private Dummy() {
super(SourceData.emptyRecord("dummy-name"));
}
private static String sourceName(String name, String namespace) {
return getSourceName(name, namespace);
}
}
}

View File

@@ -105,7 +105,7 @@ class ConfigFailFastEnabledButRetryDisabled {
assertThat(context.containsBean("kubernetesConfigRetryInterceptor")).isFalse();
assertThatThrownBy(() -> propertySourceLocator.locate(new MockEnvironment()))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read ConfigMap with name 'application' in namespace 'default'");
.hasMessage("Unable to read ConfigMap(s) in namespace 'default'");
// verify that propertySourceLocator.locate is called only once
verify(propertySourceLocator, times(1)).locate(any());

View File

@@ -117,7 +117,7 @@ class ConfigRetryDisabledButSecretsRetryEnabled {
assertThat(context.containsBean("kubernetesConfigRetryInterceptor")).isTrue();
assertThatThrownBy(() -> propertySourceLocator.locate(new MockEnvironment()))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read ConfigMap with name 'application' in namespace 'default'");
.hasMessage("Unable to read ConfigMap(s) in namespace 'default'");
// verify that propertySourceLocator.locate is called only once
verify(propertySourceLocator, times(1)).locate(any());

View File

@@ -164,7 +164,7 @@ class ConfigRetryEnabled {
assertThatThrownBy(() -> propertySourceLocator.locate(new MockEnvironment()))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read ConfigMap with name 'application' in namespace 'default'");
.hasMessage("Unable to read ConfigMap(s) in namespace 'default'");
// verify retried 5 times until failure
verify(propertySourceLocator, times(5)).locate(any());

View File

@@ -111,7 +111,7 @@ class SecretsFailFastEnabledButRetryDisabled {
assertThat(context.containsBean("kubernetesSecretsRetryInterceptor")).isFalse();
assertThatThrownBy(() -> propertySourceLocator.locate(new MockEnvironment()))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read Secret with name 'my-secret' or labels [{}] in namespace 'default'");
.hasMessage("Unable to read Secret with name 'my-secret' in namespace 'default'");
// verify that propertySourceLocator.locate is called only once
verify(propertySourceLocator, times(1)).locate(any());

View File

@@ -118,7 +118,7 @@ class SecretsRetryDisabledButConfigRetryEnabled {
assertThat(context.containsBean("kubernetesSecretsRetryInterceptor")).isTrue();
assertThatThrownBy(() -> propertySourceLocator.locate(new MockEnvironment()))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read Secret with name 'my-secret' or labels [{}] in namespace 'default'");
.hasMessage("Unable to read Secret with name 'my-secret' in namespace 'default'");
// verify that propertySourceLocator.locate is called only once
verify(propertySourceLocator, times(1)).locate(any());

View File

@@ -164,7 +164,7 @@ class SecretsRetryEnabled {
assertThatThrownBy(() -> propertySourceLocator.locate(new MockEnvironment()))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read Secret with name 'my-secret' or labels [{}] in namespace 'default'");
.hasMessage("Unable to read Secret with name 'my-secret' in namespace 'default'");
// verify retried 5 times until failure
verify(propertySourceLocator, times(5)).locate(any());

View File

@@ -10,8 +10,20 @@
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-kubernetes-commons</artifactId>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>

View File

@@ -40,8 +40,6 @@ public abstract class AbstractConfigProperties {
protected RetryProperties retry = new RetryProperties();
public abstract String getConfigurationTarget();
public boolean isEnabled() {
return this.enabled;
}

View File

@@ -88,18 +88,15 @@ public class ConfigMapConfigProperties extends AbstractConfigProperties {
"'spring.cloud.kubernetes.config.useNameAsPrefix' is set to 'true', but 'spring.cloud.kubernetes.config.sources'"
+ " is empty; as such will default 'useNameAsPrefix' to 'false'");
}
return Collections.singletonList(new NormalizedSource(name, namespace, "", includeProfileSpecificSources));
return Collections.singletonList(
new NamedConfigMapNormalizedSource(name, namespace, failFast, "", includeProfileSpecificSources));
}
return sources.stream().map(s -> s.normalize(name, namespace, useNameAsPrefix, includeProfileSpecificSources))
return sources.stream()
.map(s -> s.normalize(name, namespace, useNameAsPrefix, includeProfileSpecificSources, failFast))
.collect(Collectors.toList());
}
@Override
public String getConfigurationTarget() {
return "Config Map";
}
/**
* Config map source.
*/
@@ -136,11 +133,6 @@ public class ConfigMapConfigProperties extends AbstractConfigProperties {
}
public Source(String name, String namespace) {
this.name = name;
this.namespace = namespace;
}
public String getName() {
return this.name;
}
@@ -185,15 +177,16 @@ public class ConfigMapConfigProperties extends AbstractConfigProperties {
return !StringUtils.hasLength(this.name) && !StringUtils.hasLength(this.namespace);
}
public NormalizedSource normalize(String defaultName, String defaultNamespace, boolean defaultUseNameAsPrefix,
boolean defaultIncludeProfileSpecificSources) {
private NormalizedSource normalize(String defaultName, String defaultNamespace, boolean defaultUseNameAsPrefix,
boolean defaultIncludeProfileSpecificSources, boolean failFast) {
String normalizedName = StringUtils.hasLength(this.name) ? this.name : defaultName;
String normalizedNamespace = StringUtils.hasLength(this.namespace) ? this.namespace : defaultNamespace;
String prefix = ConfigUtils.findPrefix(this.explicitPrefix, useNameAsPrefix, defaultUseNameAsPrefix,
normalizedName);
boolean includeProfileSpecificSources = ConfigUtils.includeProfileSpecificSources(
defaultIncludeProfileSpecificSources, this.includeProfileSpecificSources);
return new NormalizedSource(normalizedName, normalizedNamespace, prefix, includeProfileSpecificSources);
return new NamedConfigMapNormalizedSource(normalizedName, normalizedNamespace, failFast, prefix,
includeProfileSpecificSources);
}
@Override
@@ -215,61 +208,4 @@ public class ConfigMapConfigProperties extends AbstractConfigProperties {
}
public static class NormalizedSource {
private final String name;
private final String namespace;
private final String prefix;
private final boolean includeProfileSpecificSources;
NormalizedSource(String name, String namespace, String prefix, boolean includeProfileSpecificSources) {
this.name = name;
this.namespace = namespace;
this.prefix = Objects.requireNonNull(prefix);
this.includeProfileSpecificSources = includeProfileSpecificSources;
}
public String getName() {
return this.name;
}
public String getNamespace() {
return this.namespace;
}
public String getPrefix() {
return prefix;
}
public boolean isIncludeProfileSpecificSources() {
return includeProfileSpecificSources;
}
@Override
public String toString() {
return "{ config-map name : '" + name + "', namespace : '" + namespace + "', prefix : '" + prefix + "' }";
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
NormalizedSource other = (NormalizedSource) o;
return Objects.equals(this.name, other.name) && Objects.equals(this.namespace, other.namespace);
}
@Override
public int hashCode() {
return Objects.hash(name, namespace);
}
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2013-2022 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.commons.config;
import java.util.Map;
import java.util.Set;
/**
* A holder for data needed to compute prefix based properties, in case of a config map.
*
* @author wind57
*/
public final record ConfigMapPrefixContext(Map<String, Object> data, String prefix, String namespace,
Set<String> propertySourceNames) {
}

View File

@@ -27,6 +27,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.util.CollectionUtils;
import static org.springframework.cloud.kubernetes.commons.config.Constants.APPLICATION_PROPERTIES;
import static org.springframework.cloud.kubernetes.commons.config.Constants.APPLICATION_YAML;
@@ -49,11 +50,11 @@ public abstract class ConfigMapPropertySource extends MapPropertySource {
private static final Log LOG = LogFactory.getLog(ConfigMapPropertySource.class);
public ConfigMapPropertySource(String name, Map<String, Object> source) {
super(name, source);
public ConfigMapPropertySource(SourceData sourceData) {
super(sourceData.sourceName(), sourceData.sourceData());
}
protected static String getName(String applicationName, String namespace) {
protected static String getSourceName(String applicationName, String namespace) {
return PREFIX + PROPERTY_SOURCE_NAME_SEPARATOR + applicationName + PROPERTY_SOURCE_NAME_SEPARATOR + namespace;
}
@@ -79,15 +80,27 @@ public abstract class ConfigMapPropertySource extends MapPropertySource {
return defaultProcessAllEntries(input, environment);
}
protected static Map<String, Object> defaultProcessAllEntries(Map<String, String> input, Environment environment) {
/*
* this method will return a SourceData that has a name in the form :
* "configmap.my-configmap.my-configmap-2.namespace" and the "data" from the context
* is appended with prefix. So if incoming is "a=b", the result will be : "prefix.a=b"
*/
protected static SourceData withPrefix(ConfigMapPrefixContext context) {
Map<String, Object> withPrefix = CollectionUtils.newHashMap(context.data().size());
context.data().forEach((key, value) -> withPrefix.put(context.prefix() + "." + key, value));
String propertySourceTokens = String.join(PROPERTY_SOURCE_NAME_SEPARATOR, context.propertySourceNames());
return new SourceData(getSourceName(propertySourceTokens, context.namespace()), withPrefix);
}
private static Map<String, Object> defaultProcessAllEntries(Map<String, String> input, Environment environment) {
return input.entrySet().stream().map(e -> extractProperties(e.getKey(), e.getValue(), environment))
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, throwingMerger(), HashMap::new));
}
protected static Map<String, Object> extractProperties(String resourceName, String content,
Environment environment) {
private static Map<String, Object> extractProperties(String resourceName, String content, Environment environment) {
if (resourceName.equals(APPLICATION_YAML) || resourceName.equals(APPLICATION_YML)) {
return yamlParserGenerator(environment).andThen(PROPERTIES_TO_MAP).apply(content);

View File

@@ -31,14 +31,12 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties.NormalizedSource;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.getApplicationName;
import static org.springframework.cloud.kubernetes.commons.config.PropertySourceUtils.KEY_VALUE_TO_PROPERTIES;
import static org.springframework.cloud.kubernetes.commons.config.PropertySourceUtils.PROPERTIES_TO_MAP;
import static org.springframework.cloud.kubernetes.commons.config.PropertySourceUtils.yamlParserGenerator;
@@ -60,13 +58,12 @@ public abstract class ConfigMapPropertySourceLocator implements PropertySourceLo
this.properties = properties;
}
protected abstract MapPropertySource getMapPropertySource(String applicationName, NormalizedSource normalizedSource,
String configurationTarget, ConfigurableEnvironment environment);
protected abstract MapPropertySource getMapPropertySource(NormalizedSource normalizedSource,
ConfigurableEnvironment environment);
@Override
public PropertySource<?> locate(Environment environment) {
if (environment instanceof ConfigurableEnvironment) {
ConfigurableEnvironment env = (ConfigurableEnvironment) environment;
if (environment instanceof ConfigurableEnvironment env) {
CompositePropertySource composite = new CompositePropertySource("composite-configmap");
if (this.properties.isEnableApi()) {
@@ -90,9 +87,7 @@ public abstract class ConfigMapPropertySourceLocator implements PropertySourceLo
private MapPropertySource getMapPropertySourceForSingleConfigMap(ConfigurableEnvironment environment,
NormalizedSource normalizedSource) {
String configurationTarget = this.properties.getConfigurationTarget();
String applicationName = getApplicationName(environment, normalizedSource.getName(), configurationTarget);
return getMapPropertySource(applicationName, normalizedSource, configurationTarget, environment);
return getMapPropertySource(normalizedSource, environment);
}
private void addPropertySourcesFromPaths(Environment environment, CompositePropertySource composite) {

View File

@@ -99,4 +99,14 @@ public final class ConfigUtils {
return defaultIncludeProfileSpecificSources;
}
/**
* action to take when an Exception happens when dealing with a source.
*/
public static void onException(boolean failFast, String message, Exception e) {
if (failFast) {
throw new IllegalStateException(message, e);
}
LOG.warn(message + ". Ignoring.", e);
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2013-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.kubernetes.commons.config;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
/**
* A secret source that is based on labels.
*
* @author wind57
*/
public final class LabeledSecretNormalizedSource extends NormalizedSource {
private final Map<String, String> labels;
public LabeledSecretNormalizedSource(String namespace, Map<String, String> labels, boolean failFast) {
super(null, namespace, failFast);
this.labels = Collections.unmodifiableMap(Objects.requireNonNull(labels));
}
/**
* will return an immutable Map.
*/
public Map<String, String> labels() {
return labels;
}
@Override
public NormalizedSourceType type() {
return NormalizedSourceType.LABELED_SECRET;
}
@Override
public String target() {
return "Secret";
}
@Override
public String toString() {
return "{ secret labels : '" + labels() + "', namespace : '" + namespace() + "'";
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
LabeledSecretNormalizedSource other = (LabeledSecretNormalizedSource) o;
return Objects.equals(labels(), other.labels()) && Objects.equals(namespace(), other.namespace());
}
@Override
public int hashCode() {
return Objects.hash(labels(), namespace());
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2013-2022 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.commons.config;
import java.util.Objects;
/**
* A config map source that is based on name.
*
* @author wind57
*/
public final class NamedConfigMapNormalizedSource extends NormalizedSource {
private final String prefix;
private final boolean includeProfileSpecificSources;
public NamedConfigMapNormalizedSource(String name, String namespace, boolean failFast, String prefix,
boolean includeProfileSpecificSources) {
super(name, namespace, failFast);
this.prefix = Objects.requireNonNull(prefix);
this.includeProfileSpecificSources = includeProfileSpecificSources;
}
public String prefix() {
return prefix;
}
public boolean profileSpecificSources() {
return includeProfileSpecificSources;
}
@Override
public NormalizedSourceType type() {
return NormalizedSourceType.NAMED_CONFIG_MAP;
}
@Override
public String target() {
return "Config Map";
}
@Override
public String toString() {
return "{ config-map name : '" + name() + "', namespace : '" + namespace() + "', prefix : '" + prefix() + "' }";
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
NamedConfigMapNormalizedSource other = (NamedConfigMapNormalizedSource) o;
return Objects.equals(this.name(), other.name()) && Objects.equals(this.namespace(), other.namespace());
}
@Override
public int hashCode() {
return Objects.hash(name(), namespace());
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2013-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.kubernetes.commons.config;
import java.util.Objects;
/**
* A secret source that is based on name.
*
* @author wind57
*/
public final class NamedSecretNormalizedSource extends NormalizedSource {
public NamedSecretNormalizedSource(String name, String namespace, boolean failFast) {
super(name, namespace, failFast);
}
@Override
public NormalizedSourceType type() {
return NormalizedSourceType.NAMED_SECRET;
}
@Override
public String target() {
return "Secret";
}
@Override
public String toString() {
return "{ secret name : '" + name() + "', namespace : '" + namespace() + "'";
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
NamedSecretNormalizedSource other = (NamedSecretNormalizedSource) o;
return Objects.equals(name(), other.name()) && Objects.equals(namespace(), other.namespace());
}
@Override
public int hashCode() {
return Objects.hash(name(), namespace());
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2013-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.kubernetes.commons.config;
import java.util.Optional;
/**
* Base class for Normalized Sources. It should contain all the "normalized" properties
* that users can specify, either explicitly or implicitly.
*
* @author wind57
*/
public sealed abstract class NormalizedSource permits NamedSecretNormalizedSource,LabeledSecretNormalizedSource,NamedConfigMapNormalizedSource {
private final String namespace;
private final String name;
private final boolean failFast;
protected NormalizedSource(String name, String namespace, boolean failFast) {
this.name = name;
this.namespace = namespace;
this.failFast = failFast;
}
public final Optional<String> namespace() {
return Optional.ofNullable(this.namespace);
}
// this can return an empty Optional (for a secret based on labels, for example)
public final Optional<String> name() {
return Optional.ofNullable(this.name);
}
public final boolean failFast() {
return failFast;
}
/**
* type of this normalized source. Callers are sensitive towards the actual type
* specified.
*/
public abstract NormalizedSourceType type();
public abstract String target();
public abstract String toString();
public abstract boolean equals(Object o);
public abstract int hashCode();
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2013-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.kubernetes.commons.config;
/**
* Defines the type of the normalized source.
*
* @author wind57
*/
public enum NormalizedSourceType {
/**
* denotes the fact that this is a secret source based on name.
*/
NAMED_SECRET,
/**
* denotes the fact that this is a secret source based on labels.
*/
LABELED_SECRET,
/**
* denotes the fact that this is a config map source based on name.
*/
NAMED_CONFIG_MAP
}

View File

@@ -16,15 +16,19 @@
package org.springframework.cloud.kubernetes.commons.config;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.getApplicationName;
/**
* Properties for configuring Kubernetes secrets.
*
@@ -80,11 +84,6 @@ public class SecretsConfigProperties extends AbstractConfigProperties {
this.sources = sources;
}
@Override
public String getConfigurationTarget() {
return "Secret";
}
/**
* @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
@@ -93,14 +92,21 @@ public class SecretsConfigProperties extends AbstractConfigProperties {
* These are the actual name/namespace pairs that are used to create a
* SecretsPropertySource
*/
public List<SecretsConfigProperties.NormalizedSource> determineSources() {
public List<NormalizedSource> determineSources(Environment environment) {
if (this.sources.isEmpty()) {
return Collections
.singletonList(new SecretsConfigProperties.NormalizedSource(SecretsConfigProperties.this.name,
SecretsConfigProperties.this.namespace, SecretsConfigProperties.this.labels));
List<NormalizedSource> result = new ArrayList<>(2);
String name = getApplicationName(environment, this.name, "Secret");
result.add(new NamedSecretNormalizedSource(name, this.namespace, this.isFailFast()));
if (!labels.isEmpty()) {
result.add(new LabeledSecretNormalizedSource(this.namespace, this.labels, this.isFailFast()));
}
return result;
}
return this.sources.stream().map(s -> s.normalize(this.name, this.namespace, this.labels))
return this.sources.stream()
.flatMap(s -> s.normalize(this.name, this.namespace, this.labels, this.isFailFast(), environment))
.collect(Collectors.toList());
}
@@ -124,6 +130,7 @@ public class SecretsConfigProperties extends AbstractConfigProperties {
public Source() {
}
@Deprecated
public Source(String name, String namespace, Map<String, String> labels) {
this.name = name;
this.namespace = namespace;
@@ -158,64 +165,27 @@ public class SecretsConfigProperties extends AbstractConfigProperties {
return !StringUtils.hasLength(this.name) && !StringUtils.hasLength(this.namespace);
}
public SecretsConfigProperties.NormalizedSource normalize(String defaultName, String defaultNamespace,
Map<String, String> defaultLabels) {
private Stream<NormalizedSource> normalize(String defaultName, String defaultNamespace,
Map<String, String> defaultLabels, boolean failFast, Environment environment) {
Stream.Builder<NormalizedSource> normalizedSources = Stream.builder();
String normalizedName = StringUtils.hasLength(this.name) ? this.name : defaultName;
String normalizedNamespace = StringUtils.hasLength(this.namespace) ? this.namespace : defaultNamespace;
Map<String, String> normalizedLabels = this.labels.isEmpty() ? defaultLabels : this.labels;
return new SecretsConfigProperties.NormalizedSource(normalizedName, normalizedNamespace, normalizedLabels);
}
String secretName = getApplicationName(environment, normalizedName, "Secret");
NormalizedSource nameBasedSource = new NamedSecretNormalizedSource(secretName, normalizedNamespace,
failFast);
normalizedSources.add(nameBasedSource);
}
public static class NormalizedSource {
private final String name;
private final String namespace;
private final Map<String, String> labels;
NormalizedSource(String name, String namespace, Map<String, String> labels) {
this.name = name;
this.namespace = namespace;
this.labels = labels;
}
public String getName() {
return this.name;
}
public String getNamespace() {
return this.namespace;
}
public Map<String, String> getLabels() {
return labels;
}
@Override
public String toString() {
return "{ secret name : '" + name + "', namespace : '" + namespace + "'";
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
if (!normalizedLabels.isEmpty()) {
NormalizedSource labelsBasedSource = new LabeledSecretNormalizedSource(normalizedNamespace, labels,
failFast);
normalizedSources.add(labelsBasedSource);
}
if (o == null || getClass() != o.getClass()) {
return false;
}
SecretsConfigProperties.NormalizedSource other = (SecretsConfigProperties.NormalizedSource) o;
return Objects.equals(this.name, other.name) && Objects.equals(this.namespace, other.namespace)
&& Objects.equals(this.labels, other.labels);
}
@Override
public int hashCode() {
return Objects.hash(name, namespace, labels);
return normalizedSources.build();
}
}

View File

@@ -16,11 +16,10 @@
package org.springframework.cloud.kubernetes.commons.config;
import java.util.Base64;
import java.util.Map;
import org.springframework.core.env.MapPropertySource;
import static org.springframework.cloud.kubernetes.commons.config.Constants.PROPERTY_SOURCE_NAME_SEPARATOR;
/**
* Kubernetes property source for secrets.
*
@@ -29,17 +28,12 @@ import org.springframework.core.env.MapPropertySource;
*/
public class SecretsPropertySource extends MapPropertySource {
public SecretsPropertySource(String name, Map<String, Object> source) {
super(name, source);
public SecretsPropertySource(SourceData sourceData) {
super(sourceData.sourceName(), sourceData.sourceData());
}
protected static String getSourceName(String name, String namespace) {
return "secrets" + Constants.PROPERTY_SOURCE_NAME_SEPARATOR + name + Constants.PROPERTY_SOURCE_NAME_SEPARATOR
+ namespace;
}
protected static void putAll(Map<String, String> data, Map<String, Object> result) {
data.forEach((k, v) -> result.put(k, new String(Base64.getDecoder().decode(v)).trim()));
return "secrets" + PROPERTY_SOURCE_NAME_SEPARATOR + name + PROPERTY_SOURCE_NAME_SEPARATOR + namespace;
}
@Override

View File

@@ -64,11 +64,10 @@ public abstract class SecretsPropertySourceLocator implements PropertySourceLoca
@Override
public PropertySource<?> locate(Environment environment) {
if (environment instanceof ConfigurableEnvironment) {
ConfigurableEnvironment env = (ConfigurableEnvironment) environment;
if (environment instanceof ConfigurableEnvironment env) {
List<SecretsConfigProperties.NormalizedSource> sources = this.properties.determineSources();
Set<SecretsConfigProperties.NormalizedSource> uniqueSources = new HashSet<>(sources);
List<NormalizedSource> sources = this.properties.determineSources(environment);
Set<NormalizedSource> uniqueSources = new HashSet<>(sources);
LOG.debug("Secrets normalized sources : " + sources);
CompositePropertySource composite = new CompositePropertySource("composite-secrets");
// read for secrets mount
@@ -89,14 +88,13 @@ public abstract class SecretsPropertySourceLocator implements PropertySourceLoca
}
private MapPropertySource getMapPropertySourceForSingleSecret(ConfigurableEnvironment environment,
SecretsConfigProperties.NormalizedSource normalizedSource) {
NormalizedSource normalizedSource) {
String configurationTarget = this.properties.getConfigurationTarget();
return getPropertySource(environment, normalizedSource, configurationTarget);
return getPropertySource(environment, normalizedSource);
}
protected abstract MapPropertySource getPropertySource(ConfigurableEnvironment environment,
SecretsConfigProperties.NormalizedSource normalizedSource, String configurationTarget);
NormalizedSource normalizedSource);
protected void putPathConfig(CompositePropertySource composite) {

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2013-2022 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.commons.config;
import java.util.Collections;
import java.util.Map;
/**
* Holds the data for a certain source. For example the contents (sourceData) for secret
* with name sourceName.
*
* @author wind57
*/
public final record SourceData(String sourceName, Map<String, Object> sourceData) {
public static SourceData emptyRecord(String sourceName) {
return new SourceData(sourceName, Collections.emptyMap());
}
}

View File

@@ -47,10 +47,10 @@ public class ConfigMapConfigPropertiesTests {
properties.setName("config-map-a");
properties.setNamespace("spring-k8s");
List<ConfigMapConfigProperties.NormalizedSource> sources = properties.determineSources();
List<NormalizedSource> sources = properties.determineSources();
Assertions.assertEquals(sources.size(), 1, "empty sources must generate a List with a single NormalizedSource");
Assertions.assertEquals(sources.get(0).getPrefix(), "",
Assertions.assertEquals(((NamedConfigMapNormalizedSource) sources.get(0)).prefix(), "",
"empty sources must generate a List with a single NormalizedSource, where prefix is empty");
}
@@ -76,10 +76,10 @@ public class ConfigMapConfigPropertiesTests {
properties.setName("config-map-a");
properties.setNamespace("spring-k8s");
List<ConfigMapConfigProperties.NormalizedSource> sources = properties.determineSources();
List<NormalizedSource> sources = properties.determineSources();
Assertions.assertEquals(sources.size(), 1, "empty sources must generate a List with a single NormalizedSource");
Assertions.assertEquals(sources.get(0).getPrefix(), "",
Assertions.assertEquals(((NamedConfigMapNormalizedSource) sources.get(0)).prefix(), "",
"empty sources must generate a List with a single NormalizedSource, where prefix is empty,"
+ "no matter of 'spring.cloud.kubernetes.config.useNameAsPrefix' value");
}
@@ -109,10 +109,10 @@ public class ConfigMapConfigPropertiesTests {
one.setName("config-map-one");
properties.setSources(Collections.singletonList(one));
List<ConfigMapConfigProperties.NormalizedSource> sources = properties.determineSources();
List<NormalizedSource> sources = properties.determineSources();
Assertions.assertEquals(sources.size(), 1, "a single NormalizedSource is expected");
Assertions.assertEquals(sources.get(0).getPrefix(), "config-map-one");
Assertions.assertEquals(((NamedConfigMapNormalizedSource) sources.get(0)).prefix(), "config-map-one");
}
/**
@@ -155,12 +155,12 @@ public class ConfigMapConfigPropertiesTests {
properties.setSources(Arrays.asList(one, two, three));
List<ConfigMapConfigProperties.NormalizedSource> sources = properties.determineSources();
List<NormalizedSource> sources = properties.determineSources();
Assertions.assertEquals(sources.size(), 3, "3 NormalizedSources are expected");
Assertions.assertEquals(sources.get(0).getPrefix(), "");
Assertions.assertEquals(sources.get(1).getPrefix(), "config-map-two");
Assertions.assertEquals(sources.get(2).getPrefix(), "config-map-three");
Assertions.assertEquals(((NamedConfigMapNormalizedSource) sources.get(0)).prefix(), "");
Assertions.assertEquals(((NamedConfigMapNormalizedSource) sources.get(1)).prefix(), "config-map-two");
Assertions.assertEquals(((NamedConfigMapNormalizedSource) sources.get(2)).prefix(), "config-map-three");
}
/**
@@ -209,13 +209,13 @@ public class ConfigMapConfigPropertiesTests {
properties.setSources(Arrays.asList(one, two, three, four));
List<ConfigMapConfigProperties.NormalizedSource> sources = properties.determineSources();
List<NormalizedSource> sources = properties.determineSources();
Assertions.assertEquals(sources.size(), 4, "4 NormalizedSources are expected");
Assertions.assertEquals(sources.get(0).getPrefix(), "one");
Assertions.assertEquals(sources.get(1).getPrefix(), "two");
Assertions.assertEquals(sources.get(2).getPrefix(), "three");
Assertions.assertEquals(sources.get(3).getPrefix(), "");
Assertions.assertEquals(((NamedConfigMapNormalizedSource) sources.get(0)).prefix(), "one");
Assertions.assertEquals(((NamedConfigMapNormalizedSource) sources.get(1)).prefix(), "two");
Assertions.assertEquals(((NamedConfigMapNormalizedSource) sources.get(2)).prefix(), "three");
Assertions.assertEquals(((NamedConfigMapNormalizedSource) sources.get(3)).prefix(), "");
}
/**
@@ -239,10 +239,10 @@ public class ConfigMapConfigPropertiesTests {
properties.setName("config-map-a");
properties.setNamespace("spring-k8s");
List<ConfigMapConfigProperties.NormalizedSource> sources = properties.determineSources();
List<NormalizedSource> sources = properties.determineSources();
Assertions.assertEquals(sources.size(), 1, "empty sources must generate a List with a single NormalizedSource");
Assertions.assertTrue(sources.get(0).isIncludeProfileSpecificSources());
Assertions.assertTrue(((NamedConfigMapNormalizedSource) sources.get(0)).profileSpecificSources());
}
/**
@@ -270,10 +270,10 @@ public class ConfigMapConfigPropertiesTests {
properties.setNamespace("spring-k8s");
properties.setIncludeProfileSpecificSources(false);
List<ConfigMapConfigProperties.NormalizedSource> sources = properties.determineSources();
List<NormalizedSource> sources = properties.determineSources();
Assertions.assertEquals(sources.size(), 1, "empty sources must generate a List with a single NormalizedSource");
Assertions.assertFalse(sources.get(0).isIncludeProfileSpecificSources());
Assertions.assertFalse(((NamedConfigMapNormalizedSource) sources.get(0)).profileSpecificSources());
}
/**
@@ -320,12 +320,12 @@ public class ConfigMapConfigPropertiesTests {
properties.setSources(Arrays.asList(one, two, three));
List<ConfigMapConfigProperties.NormalizedSource> sources = properties.determineSources();
List<NormalizedSource> sources = properties.determineSources();
Assertions.assertEquals(sources.size(), 3);
Assertions.assertTrue(sources.get(0).isIncludeProfileSpecificSources());
Assertions.assertFalse(sources.get(1).isIncludeProfileSpecificSources());
Assertions.assertFalse(sources.get(2).isIncludeProfileSpecificSources());
Assertions.assertTrue(((NamedConfigMapNormalizedSource) sources.get(0)).profileSpecificSources());
Assertions.assertFalse(((NamedConfigMapNormalizedSource) sources.get(1)).profileSpecificSources());
Assertions.assertFalse(((NamedConfigMapNormalizedSource) sources.get(2)).profileSpecificSources());
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2013-2022 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.commons.config;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
/**
* @author wind57
*/
class ConfigMapPropertySourceTests {
@Test
void testWithPrefix() {
ConfigMapPrefixContext context = new ConfigMapPrefixContext(Map.of("a", "b", "c", "d"), "prefix", "namespace",
Set.of("name1", "name2"));
SourceData result = ConfigMapPropertySource.withPrefix(context);
Assertions.assertEquals(result.sourceName().length(), 31);
Assertions.assertTrue(result.sourceName().contains("name2"));
Assertions.assertTrue(result.sourceName().contains("name1"));
Assertions.assertTrue(result.sourceName().contains("configmap"));
Assertions.assertTrue(result.sourceName().contains("namespace"));
Assertions.assertEquals(result.sourceData().get("prefix.a"), "b");
Assertions.assertEquals(result.sourceData().get("prefix.c"), "d");
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2013-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.kubernetes.commons.config;
import java.util.Collections;
import java.util.Map;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
/**
* @author wind57
*/
class LabeledSecretNormalizedSourceTests {
private final Map<String, String> labels = Collections.singletonMap("a", "b");
@Test
void testEqualsAndHashCode() {
LabeledSecretNormalizedSource left = new LabeledSecretNormalizedSource("namespace", labels, false);
LabeledSecretNormalizedSource right = new LabeledSecretNormalizedSource("namespace", labels, true);
Assertions.assertEquals(left.hashCode(), right.hashCode());
Assertions.assertEquals(left, right);
}
@Test
void testType() {
LabeledSecretNormalizedSource source = new LabeledSecretNormalizedSource("namespace", labels, false);
Assertions.assertSame(source.type(), NormalizedSourceType.LABELED_SECRET);
}
@Test
void testImmutableGetLabels() {
LabeledSecretNormalizedSource source = new LabeledSecretNormalizedSource("namespace", labels, false);
Assertions.assertThrows(RuntimeException.class, () -> source.labels().put("c", "d"));
}
@Test
void testTarget() {
LabeledSecretNormalizedSource source = new LabeledSecretNormalizedSource("namespace", labels, false);
Assertions.assertEquals(source.target(), "Secret");
}
@Test
void testConstructorFields() {
LabeledSecretNormalizedSource source = new LabeledSecretNormalizedSource("namespace", labels, false);
Assertions.assertTrue(source.name().isEmpty());
Assertions.assertEquals(source.namespace().get(), "namespace");
Assertions.assertFalse(source.failFast());
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2013-2022 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.commons.config;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
/**
* @author wind57
*/
class NamedConfigMapNormalizedSourceTests {
@Test
void testEqualsAndHashCode() {
NamedConfigMapNormalizedSource left = new NamedConfigMapNormalizedSource("name", "namespace", false, "prefix",
true);
NamedConfigMapNormalizedSource right = new NamedConfigMapNormalizedSource("name", "namespace", true,
"non-equal-prefix", false);
Assertions.assertEquals(left.hashCode(), right.hashCode());
Assertions.assertEquals(left, right);
}
@Test
void testType() {
NamedConfigMapNormalizedSource one = new NamedConfigMapNormalizedSource("name", "namespace", false, "prefix",
true);
Assertions.assertSame(one.type(), NormalizedSourceType.NAMED_CONFIG_MAP);
}
@Test
void testTarget() {
NamedConfigMapNormalizedSource one = new NamedConfigMapNormalizedSource("name", "namespace", false, "prefix",
true);
Assertions.assertEquals(one.target(), "Config Map");
}
@Test
void testConstructorFields() {
NamedConfigMapNormalizedSource one = new NamedConfigMapNormalizedSource("name", "namespace", false, "prefix",
true);
Assertions.assertEquals(one.name().get(), "name");
Assertions.assertEquals(one.namespace().get(), "namespace");
Assertions.assertFalse(one.failFast());
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2013-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.kubernetes.commons.config;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
/**
* @author wind57
*/
class NamedSecretNormalizedSourceTests {
@Test
void testEqualsAndHashCode() {
NamedSecretNormalizedSource left = new NamedSecretNormalizedSource("name", "namespace", false);
NamedSecretNormalizedSource right = new NamedSecretNormalizedSource("name", "namespace", true);
Assertions.assertEquals(left.hashCode(), right.hashCode());
Assertions.assertEquals(left, right);
}
@Test
void testType() {
NamedSecretNormalizedSource source = new NamedSecretNormalizedSource("name", "namespace", false);
Assertions.assertSame(source.type(), NormalizedSourceType.NAMED_SECRET);
}
@Test
void testTarget() {
NamedSecretNormalizedSource source = new NamedSecretNormalizedSource("name", "namespace", false);
Assertions.assertEquals(source.target(), "Secret");
}
@Test
void testConstructorFields() {
NamedSecretNormalizedSource source = new NamedSecretNormalizedSource("name", "namespace", false);
Assertions.assertEquals(source.name().get(), "name");
Assertions.assertEquals(source.namespace().get(), "namespace");
Assertions.assertFalse(source.failFast());
}
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2013-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.kubernetes.commons.config;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.mock.env.MockEnvironment;
/**
* @author wind57
*/
class SecretsConfigPropertiesTests {
private final SecretsConfigProperties properties = new SecretsConfigProperties();
/**
* the case when labels are empty
*/
@Test
void emptySourcesSecretName() {
properties.setNamespace("namespace");
List<NormalizedSource> source = properties.determineSources(new MockEnvironment());
properties.setSources(Collections.emptyList());
Assertions.assertEquals(source.size(), 1);
Assertions.assertTrue(source.get(0) instanceof NamedSecretNormalizedSource);
Assertions.assertEquals(source.get(0).name().get(), "application");
}
/**
* <pre>
* spring:
* cloud:
* kubernetes:
* secrets:
* sources:
* - name : one
* labels:
* one: "1"
* - labels:
* two: 2
* - labels:
* three: 3
* </pre>
*
* proves what there are 5 normalized sources after calling normalize method and put
* the result in a Set.
*/
@Test
void multipleSources() {
SecretsConfigProperties.Source one = new SecretsConfigProperties.Source();
one.setNamespace("spring-k8s");
one.setName("one");
one.setLabels(Collections.singletonMap("one", "1"));
SecretsConfigProperties.Source two = new SecretsConfigProperties.Source();
two.setLabels(Collections.singletonMap("two", "2"));
two.setNamespace("spring-k8s");
SecretsConfigProperties.Source three = new SecretsConfigProperties.Source();
three.setLabels(Collections.singletonMap("three", "3"));
three.setNamespace("spring-k8s");
properties.setSources(Arrays.asList(one, two, three));
List<NormalizedSource> result = properties.determineSources(new MockEnvironment());
Assertions.assertEquals(result.size(), 6);
Set<NormalizedSource> resultAsSet = new LinkedHashSet<>(result);
Assertions.assertEquals(resultAsSet.size(), 5);
Iterator<NormalizedSource> iterator = resultAsSet.iterator();
NormalizedSource oneResult = iterator.next();
Assertions.assertEquals(oneResult.name().get(), "one");
NormalizedSource twoResult = iterator.next();
Assertions.assertEquals(((LabeledSecretNormalizedSource) twoResult).labels(),
Collections.singletonMap("one", "1"));
NormalizedSource threeResult = iterator.next();
Assertions.assertEquals(threeResult.name().get(), "application");
NormalizedSource fourResult = iterator.next();
Assertions.assertEquals(((LabeledSecretNormalizedSource) fourResult).labels(),
Collections.singletonMap("two", "2"));
NormalizedSource fiveResult = iterator.next();
Assertions.assertEquals(((LabeledSecretNormalizedSource) fiveResult).labels(),
Collections.singletonMap("three", "3"));
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2013-2022 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.commons.config;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
/**
* @author wind57
*/
class SourceDataTests {
@Test
void testEmpty() {
Assertions.assertEquals(SourceData.emptyRecord("name").sourceData().size(), 0);
}
}

View File

@@ -17,7 +17,6 @@
package org.springframework.cloud.kubernetes.configserver;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import io.kubernetes.client.openapi.apis.CoreV1Api;
@@ -31,11 +30,15 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.cloud.config.server.config.ConfigServerAutoConfiguration;
import org.springframework.cloud.config.server.environment.EnvironmentRepository;
import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration;
import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigContext;
import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySource;
import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsPropertySource;
import org.springframework.cloud.kubernetes.commons.ConditionalOnKubernetesConfigEnabled;
import org.springframework.cloud.kubernetes.commons.ConditionalOnKubernetesSecretsEnabled;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.config.NamedConfigMapNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.NamedSecretNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@@ -70,8 +73,16 @@ public class KubernetesConfigServerAutoConfiguration {
return (coreApi, applicationName, namespace, springEnv) -> {
List<String> namespaces = namespaceSplitter(properties.getConfigMapNamespaces(), namespace);
List<MapPropertySource> propertySources = new ArrayList<>();
namespaces.forEach(space -> propertySources.add(new KubernetesClientConfigMapPropertySource(coreApi,
applicationName, space, springEnv, "", true, false)));
namespaces.forEach(space -> {
NamedConfigMapNormalizedSource source = new NamedConfigMapNormalizedSource(applicationName, space,
false, "", true);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(coreApi, source, space,
springEnv);
propertySources.add(new KubernetesClientConfigMapPropertySource(context));
});
return propertySources;
};
}
@@ -83,8 +94,14 @@ public class KubernetesConfigServerAutoConfiguration {
return (coreApi, applicationName, namespace, springEnv) -> {
List<String> namespaces = namespaceSplitter(properties.getSecretsNamespaces(), namespace);
List<MapPropertySource> propertySources = new ArrayList<>();
namespaces.forEach(space -> propertySources.add(new KubernetesClientSecretsPropertySource(coreApi,
applicationName, space, new HashMap<>(), false)));
namespaces.forEach(space -> {
NormalizedSource source = new NamedSecretNormalizedSource(applicationName, space, false);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(coreApi, source, space,
springEnv);
propertySources.add(new KubernetesClientSecretsPropertySource(context));
});
return propertySources;
};
}

View File

@@ -16,7 +16,6 @@
package org.springframework.cloud.kubernetes.configserver;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import io.kubernetes.client.openapi.JSON;
import io.kubernetes.client.openapi.models.V1ConfigMapBuilder;
@@ -51,8 +50,6 @@ public class ConfigServerIntegrationTest {
@Autowired
private TestRestTemplate testRestTemplate;
public static WireMockServer wireMockServer;
@BeforeEach
public void beforeEach() {
V1ConfigMapList TEST_CONFIGMAP = new V1ConfigMapList().addItemsItem(new V1ConfigMapBuilder().withMetadata(

View File

@@ -17,7 +17,6 @@
package org.springframework.cloud.kubernetes.configserver;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import io.kubernetes.client.openapi.ApiException;
@@ -28,12 +27,17 @@ import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder;
import io.kubernetes.client.openapi.models.V1SecretBuilder;
import io.kubernetes.client.openapi.models.V1SecretList;
import io.kubernetes.client.openapi.models.V1SecretListBuilder;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigContext;
import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySource;
import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsPropertySource;
import org.springframework.cloud.kubernetes.commons.config.NamedConfigMapNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.NamedSecretNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSource;
import org.springframework.core.env.MapPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
@@ -96,16 +100,28 @@ class KubernetesEnvironmentRepositoryTests {
public static void before() {
kubernetesPropertySourceSuppliers.add((coreApi, applicationName, namespace, springEnv) -> {
List<MapPropertySource> propertySources = new ArrayList<>();
propertySources.add(new KubernetesClientConfigMapPropertySource(coreApi, applicationName, "default",
springEnv, "", true, false));
propertySources.add(new KubernetesClientConfigMapPropertySource(coreApi, applicationName, "dev", springEnv,
"", true, false));
NormalizedSource defaultSource = new NamedConfigMapNormalizedSource(applicationName, "default", false, "",
true);
KubernetesClientConfigContext defaultContext = new KubernetesClientConfigContext(coreApi, defaultSource,
"default", springEnv);
NormalizedSource devSource = new NamedConfigMapNormalizedSource(applicationName, "dev", false, "", true);
KubernetesClientConfigContext devContext = new KubernetesClientConfigContext(coreApi, devSource, "dev",
springEnv);
propertySources.add(new KubernetesClientConfigMapPropertySource(defaultContext));
propertySources.add(new KubernetesClientConfigMapPropertySource(devContext));
return propertySources;
});
kubernetesPropertySourceSuppliers.add((coreApi, applicationName, namespace, springEnv) -> {
List<MapPropertySource> propertySources = new ArrayList<>();
propertySources.add(new KubernetesClientSecretsPropertySource(coreApi, applicationName, "default",
new HashMap<>(), false));
NormalizedSource source = new NamedSecretNormalizedSource(applicationName, "default", false);
KubernetesClientConfigContext context = new KubernetesClientConfigContext(coreApi, source, "default",
springEnv);
propertySources.add(new KubernetesClientSecretsPropertySource(context));
return propertySources;
});
}
@@ -206,7 +222,7 @@ class KubernetesEnvironmentRepositoryTests {
environment.getPropertySources().forEach(propertySource -> {
assertThat(propertySource.getName().equals("configmap.application.default")
|| propertySource.getName().equals("secrets.application.default")
|| propertySource.getName().equals("configmap.stores.default")
|| propertySource.getName().equals("configmap.stores.stores-dev.default")
|| propertySource.getName().equals("configmap.stores.dev")
|| propertySource.getName().equals("secrets.stores.default")).isTrue();
if (propertySource.getName().equals("configmap.application.default")) {
@@ -215,19 +231,19 @@ class KubernetesEnvironmentRepositoryTests {
assertThat(propertySource.getSource().get("dummy.property.bool2")).isEqualTo(true);
assertThat(propertySource.getSource().get("dummy.property.string2")).isEqualTo("a");
}
if (propertySource.getName().equals("secrets.application.default")) {
else if (propertySource.getName().equals("secrets.application.default")) {
assertThat(propertySource.getSource().size()).isEqualTo(2);
assertThat(propertySource.getSource().get("username")).isEqualTo("user");
assertThat(propertySource.getSource().get("password")).isEqualTo("p455w0rd");
}
if (propertySource.getName().equals("configmap.stores.default")) {
else if (propertySource.getName().equals("configmap.stores.stores-dev.default")) {
assertThat(propertySource.getSource().size()).isEqualTo(4);
assertThat(propertySource.getSource().get("dummy.property.int2")).isEqualTo(2);
assertThat(propertySource.getSource().get("dummy.property.bool2")).isEqualTo(false);
assertThat(propertySource.getSource().get("dummy.property.string2")).isEqualTo("b");
assertThat(propertySource.getSource().get("dummy.property.string1")).isEqualTo("a");
}
if (propertySource.getName().equals("configmap.stores.dev")) {
else if (propertySource.getName().equals("configmap.stores.dev")) {
assertThat(propertySource.getSource().size()).isEqualTo(3);
assertThat(propertySource.getSource().get("dummy.property.int2")).isEqualTo(1);
assertThat(propertySource.getSource().get("dummy.property.bool2")).isEqualTo(true);
@@ -236,11 +252,14 @@ class KubernetesEnvironmentRepositoryTests {
// Currently KubernetesClientSecretsPropertySource does not take into account
// profiles, so that plays no role at the moment
// See https://github.com/spring-cloud/spring-cloud-kubernetes/issues/880
if (propertySource.getName().equals("secrets.stores.default")) {
else if (propertySource.getName().equals("secrets.stores.default")) {
assertThat(propertySource.getSource().size()).isEqualTo(2);
assertThat(propertySource.getSource().get("username")).isEqualTo("stores");
assertThat(propertySource.getSource().get("password")).isEqualTo("p455w0rd");
}
else {
Assertions.fail("no match in property source names");
}
});
}

View File

@@ -52,7 +52,7 @@ import org.springframework.core.env.Environment;
public class Fabric8BootstrapConfiguration {
@Bean
public KubernetesNamespaceProvider provider(Environment env) {
KubernetesNamespaceProvider provider(Environment env) {
return new KubernetesNamespaceProvider(env);
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2013-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.kubernetes.fabric8.config;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSource;
import org.springframework.core.env.Environment;
/**
* A context/holder for various data needed to compute property sources. This can be seen
* as an enhanced NormalizedSource.
*
* @author wind57
*/
final record Fabric8ConfigContext(KubernetesClient client, NormalizedSource normalizedSource, String namespace,
Environment environment) {
}

View File

@@ -16,21 +16,13 @@
package org.springframework.cloud.kubernetes.fabric8.config;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.util.EnumMap;
import java.util.Optional;
import org.springframework.cloud.kubernetes.commons.config.ConfigMapPropertySource;
import org.springframework.core.env.Environment;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSourceType;
import org.springframework.cloud.kubernetes.commons.config.SourceData;
import org.springframework.core.env.MapPropertySource;
import org.springframework.util.CollectionUtils;
import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigUtils.getApplicationNamespace;
import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigUtils.getConfigMapData;
/**
* A {@link MapPropertySource} that uses Kubernetes config maps.
@@ -40,52 +32,33 @@ import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigU
* @author Michael Moudatsos
* @author Isik Erhan
*/
public class Fabric8ConfigMapPropertySource extends ConfigMapPropertySource {
public final class Fabric8ConfigMapPropertySource extends ConfigMapPropertySource {
private static final Log LOG = LogFactory.getLog(Fabric8ConfigMapPropertySource.class);
private static final EnumMap<NormalizedSourceType, Fabric8ContextToSourceData> STRATEGIES = new EnumMap<>(
NormalizedSourceType.class);
public Fabric8ConfigMapPropertySource(KubernetesClient client, String name, String namespace,
Environment environment, String prefix, boolean includeProfileSpecificSources, boolean failFast) {
super(getName(name, getApplicationNamespace(client, namespace, "Config Map", null)),
getData(client, name, getApplicationNamespace(client, namespace, "Config Map", null), environment,
prefix, includeProfileSpecificSources, failFast));
// there is a single strategy here at the moment (unlike secrets),
// but this can change.
// to be on par with secrets implementation, I am keeping it the same
static {
STRATEGIES.put(NormalizedSourceType.NAMED_CONFIG_MAP, namedConfigMap());
}
private static Map<String, Object> getData(KubernetesClient client, String name, String namespace,
Environment environment, String prefix, boolean includeProfileSpecificSources, boolean failFast) {
Fabric8ConfigMapPropertySource(Fabric8ConfigContext context) {
super(getSourceData(context));
}
LOG.debug("Loading ConfigMap with name '" + name + "' in namespace '" + namespace + "'");
try {
Map<String, String> data = getConfigMapData(client, namespace, name);
Map<String, Object> result = new HashMap<>(processAllEntries(data, environment));
private static SourceData getSourceData(Fabric8ConfigContext context) {
NormalizedSourceType type = context.normalizedSource().type();
return Optional.ofNullable(STRATEGIES.get(type)).map(x -> x.apply(context))
.orElseThrow(() -> new IllegalArgumentException("no strategy found for : " + type));
}
if (environment != null && includeProfileSpecificSources) {
for (String activeProfile : environment.getActiveProfiles()) {
String mapNameWithProfile = name + "-" + activeProfile;
Map<String, String> dataWithProfile = getConfigMapData(client, namespace, mapNameWithProfile);
result.putAll(processAllEntries(dataWithProfile, environment));
}
}
if (!"".equals(prefix)) {
Map<String, Object> withPrefix = CollectionUtils.newHashMap(result.size());
result.forEach((key, value) -> withPrefix.put(prefix + "." + key, value));
return withPrefix;
}
return result;
}
catch (Exception e) {
if (failFast) {
throw new IllegalStateException(
"Unable to read ConfigMap with name '" + name + "' in namespace '" + namespace + "'", e);
}
LOG.warn("Can't read configMap with name: [" + name + "] in namespace: [" + namespace + "]. Ignoring.", e);
}
return Collections.emptyMap();
// we need to pass various functions because the code we are interested in
// is protected in ConfigMapPropertySource, and must stay that way.
private static Fabric8ContextToSourceData namedConfigMap() {
return NamedConfigMapContextToSourceDataProvider.of(ConfigMapPropertySource::processAllEntries,
ConfigMapPropertySource::getSourceName, ConfigMapPropertySource::withPrefix).get();
}
}

View File

@@ -21,8 +21,8 @@ import io.fabric8.kubernetes.client.KubernetesClient;
import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties;
import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties.NormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.ConfigMapPropertySourceLocator;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSource;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
@@ -43,7 +43,7 @@ public class Fabric8ConfigMapPropertySourceLocator extends ConfigMapPropertySour
private final KubernetesNamespaceProvider provider;
public Fabric8ConfigMapPropertySourceLocator(KubernetesClient client, ConfigMapConfigProperties properties,
Fabric8ConfigMapPropertySourceLocator(KubernetesClient client, ConfigMapConfigProperties properties,
KubernetesNamespaceProvider provider) {
super(properties);
this.client = client;
@@ -51,13 +51,14 @@ public class Fabric8ConfigMapPropertySourceLocator extends ConfigMapPropertySour
}
@Override
protected MapPropertySource getMapPropertySource(String applicationName, NormalizedSource normalizedSource,
String configurationTarget, ConfigurableEnvironment environment) {
String namespace = getApplicationNamespace(this.client, normalizedSource.getNamespace(), configurationTarget,
provider);
return new Fabric8ConfigMapPropertySource(this.client, applicationName, namespace, environment,
normalizedSource.getPrefix(), normalizedSource.isIncludeProfileSpecificSources(),
this.properties.isFailFast());
protected MapPropertySource getMapPropertySource(NormalizedSource normalizedSource,
ConfigurableEnvironment environment) {
// NormalizedSource has a namespace, but users can skip it.
// In such cases we try to get it elsewhere
String namespace = getApplicationNamespace(this.client, normalizedSource.namespace().orElse(null),
normalizedSource.target(), provider);
Fabric8ConfigContext context = new Fabric8ConfigContext(client, normalizedSource, namespace, environment);
return new Fabric8ConfigMapPropertySource(context);
}
}

View File

@@ -16,16 +16,20 @@
package org.springframework.cloud.kubernetes.fabric8.config;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.config.NamespaceResolutionFailedException;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
@@ -33,7 +37,7 @@ import org.springframework.util.StringUtils;
*
* @author Ioannis Canellos
*/
public final class Fabric8ConfigUtils {
final class Fabric8ConfigUtils {
private static final Log LOG = LogFactory.getLog(Fabric8ConfigUtils.class);
@@ -66,7 +70,7 @@ public final class Fabric8ConfigUtils {
KubernetesNamespaceProvider provider) {
if (StringUtils.hasText(namespace)) {
LOG.debug(configurationTarget + " namespace from normalized source or passed directly : " + namespace);
LOG.debug(configurationTarget + " namespace from normalized source : " + namespace);
return namespace;
}
@@ -102,4 +106,18 @@ public final class Fabric8ConfigUtils {
return configMap.getData();
}
/**
* return decoded data from a secret within a namespace.
*/
static Map<String, Object> dataFromSecret(Secret secret, String namespace) {
LOG.debug("reading secret with name : " + secret.getMetadata().getName() + " in namespace : " + namespace);
return secretData(secret.getData());
}
private static Map<String, Object> secretData(Map<String, String> data) {
Map<String, Object> result = new HashMap<>(CollectionUtils.newHashMap(data.size()));
data.forEach((key, value) -> result.put(key, new String(Base64.getDecoder().decode(value)).trim()));
return result;
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2013-2022 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.fabric8.config;
import java.util.function.Function;
import org.springframework.cloud.kubernetes.commons.config.SourceData;
/**
* A more succinct way to define a Function from Fabric8ConfigContext to SourceData.
*
* @author wind57
*/
interface Fabric8ContextToSourceData extends Function<Fabric8ConfigContext, SourceData> {
}

View File

@@ -16,17 +16,12 @@
package org.springframework.cloud.kubernetes.fabric8.config;
import java.util.HashMap;
import java.util.Map;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.util.EnumMap;
import java.util.Optional;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSourceType;
import org.springframework.cloud.kubernetes.commons.config.SecretsPropertySource;
import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigUtils.getApplicationNamespace;
import org.springframework.cloud.kubernetes.commons.config.SourceData;
/**
* Kubernetes property source for secrets.
@@ -35,54 +30,32 @@ import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigU
* @author Haytham Mohamed
* @author Isik Erhan
*/
public class Fabric8SecretsPropertySource extends SecretsPropertySource {
public final class Fabric8SecretsPropertySource extends SecretsPropertySource {
private static final Log LOG = LogFactory.getLog(Fabric8SecretsPropertySource.class);
private static final EnumMap<NormalizedSourceType, Fabric8ContextToSourceData> STRATEGIES = new EnumMap<>(
NormalizedSourceType.class);
public Fabric8SecretsPropertySource(KubernetesClient client, String name, String namespace,
Map<String, String> labels, boolean failFast) {
super(getSourceName(name, getApplicationNamespace(client, namespace, "Secret", null)), getSourceData(client,
name, getApplicationNamespace(client, namespace, "Secret", null), labels, failFast));
static {
STRATEGIES.put(NormalizedSourceType.NAMED_SECRET, namedSecret());
STRATEGIES.put(NormalizedSourceType.LABELED_SECRET, labeledSecret());
}
private static Map<String, Object> getSourceData(KubernetesClient client, String name, String namespace,
Map<String, String> labels, boolean failFast) {
Map<String, Object> result = new HashMap<>();
LOG.debug("Loading Secret with name '" + name + "' or with labels [" + labels + "] in namespace '" + namespace
+ "'");
try {
Secret secret = client.secrets().inNamespace(namespace).withName(name).get();
// the API is documented that it might return null
if (secret == null) {
LOG.warn("secret with name : " + name + " in namespace : " + namespace + " not found");
}
else {
putDataFromSecret(secret, result, namespace);
}
client.secrets().inNamespace(namespace).withLabels(labels).list().getItems()
.forEach(s -> putDataFromSecret(s, result, namespace));
}
catch (Exception e) {
if (failFast) {
throw new IllegalStateException("Unable to read Secret with name '" + name + "' or labels [" + labels
+ "] in namespace '" + namespace + "'", e);
}
LOG.warn("Can't read secret with name: [" + name + "] or labels [" + labels + "] in namespace: ["
+ namespace + "] (cause: " + e.getMessage() + "). Ignoring");
}
return result;
Fabric8SecretsPropertySource(Fabric8ConfigContext context) {
super(getSourceData(context));
}
private static void putDataFromSecret(Secret secret, Map<String, Object> result, String namespace) {
LOG.debug("reading secret with name : " + secret.getMetadata().getName() + " in namespace : " + namespace);
putAll(secret.getData(), result);
private static SourceData getSourceData(Fabric8ConfigContext context) {
NormalizedSourceType type = context.normalizedSource().type();
return Optional.ofNullable(STRATEGIES.get(type)).map(x -> x.apply(context))
.orElseThrow(() -> new IllegalArgumentException("no strategy found for : " + type));
}
private static Fabric8ContextToSourceData namedSecret() {
return NamedSecretContextToSourceDataProvider.of(SecretsPropertySource::getSourceName).get();
}
private static Fabric8ContextToSourceData labeledSecret() {
return LabeledSecretContextToSourceDataProvider.of(SecretsPropertySource::getSourceName).get();
}
}

View File

@@ -16,19 +16,17 @@
package org.springframework.cloud.kubernetes.fabric8.config;
import java.util.Map;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties;
import org.springframework.cloud.kubernetes.commons.config.SecretsPropertySourceLocator;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.getApplicationName;
import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigUtils.getApplicationNamespace;
/**
@@ -45,7 +43,7 @@ public class Fabric8SecretsPropertySourceLocator extends SecretsPropertySourceLo
private final KubernetesNamespaceProvider provider;
public Fabric8SecretsPropertySourceLocator(KubernetesClient client, SecretsConfigProperties properties,
Fabric8SecretsPropertySourceLocator(KubernetesClient client, SecretsConfigProperties properties,
KubernetesNamespaceProvider provider) {
super(properties);
this.client = client;
@@ -54,13 +52,13 @@ public class Fabric8SecretsPropertySourceLocator extends SecretsPropertySourceLo
@Override
protected MapPropertySource getPropertySource(ConfigurableEnvironment environment,
SecretsConfigProperties.NormalizedSource normalizedSource, String configurationTarget) {
String secretName = getApplicationName(environment, normalizedSource.getName(), configurationTarget);
String secretNamespace = getApplicationNamespace(this.client, normalizedSource.getNamespace(),
configurationTarget, provider);
Map<String, String> labels = normalizedSource.getLabels();
return new Fabric8SecretsPropertySource(this.client, secretName, secretNamespace, labels,
this.properties.isFailFast());
NormalizedSource normalizedSource) {
// NormalizedSource has a namespace, but users can skip it.
// In such cases we try to get it elsewhere
String namespace = getApplicationNamespace(client, normalizedSource.namespace().orElse(null),
normalizedSource.target(), provider);
Fabric8ConfigContext context = new Fabric8ConfigContext(client, normalizedSource, namespace, environment);
return new Fabric8SecretsPropertySource(context);
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2013-2022 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.fabric8.config;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.api.model.Secret;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.kubernetes.commons.config.LabeledSecretNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.SourceData;
import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.onException;
import static org.springframework.cloud.kubernetes.commons.config.Constants.PROPERTY_SOURCE_NAME_SEPARATOR;
import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigUtils.dataFromSecret;
/**
* Provides an implementation of {@link Fabric8ContextToSourceData} for a labeled secret.
*
* @author wind57
*/
final class LabeledSecretContextToSourceDataProvider implements Supplier<Fabric8ContextToSourceData> {
private static final Log LOG = LogFactory.getLog(LabeledSecretContextToSourceDataProvider.class);
private final BiFunction<String, String, String> sourceNameMapper;
private LabeledSecretContextToSourceDataProvider(BiFunction<String, String, String> sourceNameFunction) {
this.sourceNameMapper = Objects.requireNonNull(sourceNameFunction);
}
static LabeledSecretContextToSourceDataProvider of(BiFunction<String, String, String> sourceNameFunction) {
return new LabeledSecretContextToSourceDataProvider(sourceNameFunction);
}
/*
* Computes a ContextSourceData (think content) for secret(s) based on some labels.
* There could be many secrets that are read based on incoming labels, for which we
* will be computing a single Map<String, Object> in the end.
*
* If there is no secret found for the provided labels, we will return an "empty"
* SourceData. Its name is going to be the concatenated labels mapped to an empty Map.
*
* If we find secret(s) for the provided labels, its name is going to be the
* concatenated secret names mapped to the data they hold as a Map.
*/
@Override
public Fabric8ContextToSourceData get() {
return context -> {
LabeledSecretNormalizedSource source = ((LabeledSecretNormalizedSource) context.normalizedSource());
Map<String, String> labels = source.labels();
Map<String, Object> result = new HashMap<>();
String namespace = context.namespace();
String sourceName = String.join(PROPERTY_SOURCE_NAME_SEPARATOR, labels.keySet());
try {
LOG.info("Loading Secret(s) with labels '" + labels + "' in namespace '" + namespace + "'");
List<Secret> secrets = context.client().secrets().inNamespace(namespace).withLabels(labels).list()
.getItems();
if (!secrets.isEmpty()) {
secrets.forEach(secret -> result.putAll(dataFromSecret(secret, namespace)));
sourceName = secrets.stream().map(Secret::getMetadata).map(ObjectMeta::getName)
.collect(Collectors.joining(PROPERTY_SOURCE_NAME_SEPARATOR));
}
else {
LOG.info("No Secret(s) with labels '" + labels + "' in namespace '" + namespace + "' found.");
}
}
catch (Exception e) {
String message = "Unable to read Secret with labels [" + labels + "] in namespace '" + namespace + "'";
onException(source.failFast(), message, e);
}
String propertySourceName = sourceNameMapper.apply(sourceName, namespace);
return new SourceData(propertySourceName, result);
};
}
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright 2013-2022 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.fabric8.config;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.kubernetes.commons.config.ConfigMapPrefixContext;
import org.springframework.cloud.kubernetes.commons.config.NamedConfigMapNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.SourceData;
import org.springframework.core.env.Environment;
import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.getApplicationName;
import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.onException;
import static org.springframework.cloud.kubernetes.commons.config.Constants.PROPERTY_SOURCE_NAME_SEPARATOR;
import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigUtils.getConfigMapData;
/**
* Provides an implementation of {@link Fabric8ContextToSourceData} for a named config
* map.
*
* @author wind57
*/
final class NamedConfigMapContextToSourceDataProvider implements Supplier<Fabric8ContextToSourceData> {
private static final Log LOG = LogFactory.getLog(NamedConfigMapContextToSourceDataProvider.class);
private final BiFunction<Map<String, String>, Environment, Map<String, Object>> entriesProcessor;
private final BiFunction<String, String, String> sourceNameMapper;
private final Function<ConfigMapPrefixContext, SourceData> withPrefix;
private NamedConfigMapContextToSourceDataProvider(
BiFunction<Map<String, String>, Environment, Map<String, Object>> entriesProcessor,
BiFunction<String, String, String> sourceNameMapper,
Function<ConfigMapPrefixContext, SourceData> withPrefix) {
this.entriesProcessor = Objects.requireNonNull(entriesProcessor);
this.sourceNameMapper = Objects.requireNonNull(sourceNameMapper);
this.withPrefix = Objects.requireNonNull(withPrefix);
}
static NamedConfigMapContextToSourceDataProvider of(
BiFunction<Map<String, String>, Environment, Map<String, Object>> entriesProcessor,
BiFunction<String, String, String> sourceNameMapper,
Function<ConfigMapPrefixContext, SourceData> withPrefix) {
return new NamedConfigMapContextToSourceDataProvider(entriesProcessor, sourceNameMapper, withPrefix);
}
/*
* Computes a ContextToSourceData (think content) for config map(s) based on name.
* There could be potentially many config maps read (we also read profile based
* sources). In such a case the name of the property source is going to be the
* concatenated config map names, while the value is all the data that those config
* maps hold.
*/
@Override
public Fabric8ContextToSourceData get() {
return context -> {
NamedConfigMapNormalizedSource source = (NamedConfigMapNormalizedSource) context.normalizedSource();
String namespace = context.namespace();
String initialConfigMapName = appName(context.environment(), source).get();
String currentConfigMapName = initialConfigMapName;
Set<String> propertySourceNames = new LinkedHashSet<>();
propertySourceNames.add(initialConfigMapName);
Map<String, Object> result = new HashMap<>();
LOG.info("Loading ConfigMap with name '" + initialConfigMapName + "' in namespace '" + namespace + "'");
try {
Map<String, String> data = getConfigMapData(context.client(), namespace, currentConfigMapName);
result.putAll(entriesProcessor.apply(data, context.environment()));
if (context.environment() != null && source.profileSpecificSources()) {
for (String activeProfile : context.environment().getActiveProfiles()) {
currentConfigMapName = initialConfigMapName + "-" + activeProfile;
Map<String, String> dataWithProfile = getConfigMapData(context.client(), namespace,
currentConfigMapName);
if (!dataWithProfile.isEmpty()) {
propertySourceNames.add(currentConfigMapName);
result.putAll(entriesProcessor.apply(dataWithProfile, context.environment()));
}
}
}
if (!"".equals(source.prefix())) {
ConfigMapPrefixContext prefixContext = new ConfigMapPrefixContext(result, source.prefix(),
namespace, propertySourceNames);
return withPrefix.apply(prefixContext);
}
}
catch (Exception e) {
String message = "Unable to read ConfigMap with name '" + currentConfigMapName + "' in namespace '"
+ namespace + "'";
onException(source.failFast(), message, e);
}
String names = String.join(PROPERTY_SOURCE_NAME_SEPARATOR, propertySourceNames);
return new SourceData(sourceNameMapper.apply(names, namespace), result);
};
}
private Supplier<String> appName(Environment environment, NormalizedSource normalizedSource) {
return () -> getApplicationName(environment, normalizedSource.name().orElse(null), normalizedSource.target());
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2013-2022 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.fabric8.config;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import io.fabric8.kubernetes.api.model.Secret;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.kubernetes.commons.config.NamedSecretNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.SourceData;
import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.onException;
import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigUtils.dataFromSecret;
/**
* Provides an implementation of {@link Fabric8ContextToSourceData} for a named secret.
*
* @author wind57
*/
final class NamedSecretContextToSourceDataProvider implements Supplier<Fabric8ContextToSourceData> {
private static final Log LOG = LogFactory.getLog(LabeledSecretContextToSourceDataProvider.class);
private final BiFunction<String, String, String> sourceNameMapper;
private NamedSecretContextToSourceDataProvider(BiFunction<String, String, String> sourceNameFunction) {
this.sourceNameMapper = Objects.requireNonNull(sourceNameFunction);
}
static NamedSecretContextToSourceDataProvider of(BiFunction<String, String, String> sourceNameFunction) {
return new NamedSecretContextToSourceDataProvider(sourceNameFunction);
}
@Override
public Fabric8ContextToSourceData get() {
return context -> {
NamedSecretNormalizedSource source = (NamedSecretNormalizedSource) context.normalizedSource();
Map<String, Object> result = new HashMap<>();
// error should never be thrown here, since we always expect a name
// explicit or implicit
String secretName = source.name().orElseThrow();
String namespace = context.namespace();
try {
LOG.info("Loading Secret with name '" + secretName + "' in namespace '" + namespace + "'");
Secret secret = context.client().secrets().inNamespace(namespace).withName(secretName).get();
// the API is documented that it might return null
if (secret == null) {
LOG.warn("secret with name : " + secretName + " in namespace : " + namespace + " not found");
}
else {
result = dataFromSecret(secret, namespace);
}
}
catch (Exception e) {
String message = "Unable to read Secret with name '" + secretName + "' in namespace '" + namespace
+ "'";
onException(source.failFast(), message, e);
}
String sourceName = sourceNameMapper.apply(secretName, namespace);
return new SourceData(sourceName, result);
};
}
}

View File

@@ -25,6 +25,8 @@ import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.kubernetes.commons.config.NamedConfigMapNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSource;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
@@ -73,9 +75,10 @@ public class ConfigMapsTest {
.build();
mockClient.configMaps().inNamespace("test").create(configMap);
NormalizedSource source = new NamedConfigMapNormalizedSource(configMapName, "test", false, "", false);
Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "", new MockEnvironment());
Fabric8ConfigMapPropertySource cmps = new Fabric8ConfigMapPropertySource(mockClient, configMapName, "test",
new MockEnvironment(), "", false, false);
Fabric8ConfigMapPropertySource cmps = new Fabric8ConfigMapPropertySource(context);
assertThat(cmps.getProperty("dummy.property.string1")).isEqualTo("a");
assertThat(cmps.getProperty("dummy.property.int1")).isEqualTo("1");
@@ -89,9 +92,10 @@ public class ConfigMapsTest {
.addToData("application.yaml", ConfigMapTestUtil.readResourceFile("application.yaml")).build();
mockClient.configMaps().inNamespace("test").create(configMap);
NormalizedSource source = new NamedConfigMapNormalizedSource(configMapName, "test", false, "", false);
Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "", new MockEnvironment());
Fabric8ConfigMapPropertySource cmps = new Fabric8ConfigMapPropertySource(mockClient, configMapName, "test",
new MockEnvironment(), "", false, false);
Fabric8ConfigMapPropertySource cmps = new Fabric8ConfigMapPropertySource(context);
assertThat(cmps.getProperty("dummy.property.string2")).isEqualTo("a");
assertThat(cmps.getProperty("dummy.property.int2")).isEqualTo(1);
@@ -105,9 +109,10 @@ public class ConfigMapsTest {
.addToData("adhoc.yml", ConfigMapTestUtil.readResourceFile("adhoc.yml")).build();
mockClient.configMaps().inNamespace("test").create(configMap);
NormalizedSource source = new NamedConfigMapNormalizedSource(configMapName, "test", false, "", false);
Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "", new MockEnvironment());
Fabric8ConfigMapPropertySource cmps = new Fabric8ConfigMapPropertySource(mockClient, configMapName, "test",
new MockEnvironment(), "", false, false);
Fabric8ConfigMapPropertySource cmps = new Fabric8ConfigMapPropertySource(context);
assertThat(cmps.getProperty("dummy.property.string3")).isEqualTo("a");
assertThat(cmps.getProperty("dummy.property.int3")).isEqualTo(1);
@@ -121,9 +126,10 @@ public class ConfigMapsTest {
.addToData("application.properties", "somevalue").build();
mockClient.configMaps().inNamespace("test").create(configMap);
NormalizedSource source = new NamedConfigMapNormalizedSource(configMapName, "namespace", false, "", false);
Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "", new MockEnvironment());
Fabric8ConfigMapPropertySource cmps = new Fabric8ConfigMapPropertySource(mockClient, configMapName, "namespace",
new MockEnvironment(), "", false, false);
Fabric8ConfigMapPropertySource cmps = new Fabric8ConfigMapPropertySource(context);
// no exception is thrown for unparseable content
}
@@ -135,9 +141,10 @@ public class ConfigMapsTest {
.addToData("application.yaml", "somevalue").build();
mockClient.configMaps().inNamespace("test").create(configMap);
NormalizedSource source = new NamedConfigMapNormalizedSource(configMapName, "namespace", false, "", false);
Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "", new MockEnvironment());
Fabric8ConfigMapPropertySource cmps = new Fabric8ConfigMapPropertySource(mockClient, configMapName, "namespace",
new MockEnvironment(), "", false, false);
Fabric8ConfigMapPropertySource cmps = new Fabric8ConfigMapPropertySource(context);
// no exception is thrown for unparseable content
}
@@ -150,9 +157,10 @@ public class ConfigMapsTest {
.addToData("adhoc.properties", ConfigMapTestUtil.readResourceFile("adhoc.properties")).build();
mockClient.configMaps().inNamespace("test").create(configMap);
NormalizedSource source = new NamedConfigMapNormalizedSource(configMapName, "test", false, "", false);
Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "", new MockEnvironment());
Fabric8ConfigMapPropertySource cmps = new Fabric8ConfigMapPropertySource(mockClient, configMapName, "test",
new MockEnvironment(), "", true, false);
Fabric8ConfigMapPropertySource cmps = new Fabric8ConfigMapPropertySource(context);
// application.properties should be read correctly
assertThat(cmps.getProperty("dummy.property.string1")).isEqualTo("a");

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.kubernetes.fabric8.config.reload;
package org.springframework.cloud.kubernetes.fabric8.config;
import java.util.HashMap;
import java.util.List;
@@ -28,10 +28,11 @@ import io.fabric8.kubernetes.client.dsl.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.bootstrap.config.BootstrapPropertySource;
import org.springframework.cloud.kubernetes.commons.config.NamedConfigMapNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties;
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy;
import org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigMapPropertySource;
import org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigMapPropertySourceLocator;
import org.springframework.cloud.kubernetes.fabric8.config.reload.EventBasedConfigMapChangeDetector;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
@@ -63,8 +64,9 @@ public class EventBasedConfigurationChangeDetectorTests {
when(mixedOperation.inNamespace("default")).thenReturn(mixedOperation);
when(k8sClient.getNamespace()).thenReturn("default");
Fabric8ConfigMapPropertySource fabric8ConfigMapPropertySource = new Fabric8ConfigMapPropertySource(k8sClient,
"myconfigmap", "default", new MockEnvironment(), "", true, false);
NormalizedSource source = new NamedConfigMapNormalizedSource("myconfigmap", "default", true, "", false);
Fabric8ConfigContext context = new Fabric8ConfigContext(k8sClient, source, "default", new MockEnvironment());
Fabric8ConfigMapPropertySource fabric8ConfigMapPropertySource = new Fabric8ConfigMapPropertySource(context);
env.getPropertySources().addFirst(new BootstrapPropertySource<>(fabric8ConfigMapPropertySource));
ConfigurationUpdateStrategy configurationUpdateStrategy = mock(ConfigurationUpdateStrategy.class);

View File

@@ -16,13 +16,18 @@
package org.springframework.cloud.kubernetes.fabric8.config;
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties;
import org.springframework.cloud.kubernetes.commons.config.NamedConfigMapNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.NamespaceResolutionFailedException;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSource;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThatNoException;
@@ -32,17 +37,19 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
* @author Isik Erhan
*/
@EnableKubernetesMockClient
public class Fabric8ConfigMapPropertySourceLocatorTests {
class Fabric8ConfigMapPropertySourceLocatorTests {
KubernetesMockServer mockServer;
private KubernetesMockServer mockServer;
KubernetesClient mockClient;
private KubernetesClient mockClient;
private final DefaultKubernetesClient client = Mockito.mock(DefaultKubernetesClient.class);
@Test
public void locateShouldThrowExceptionOnFailureWhenFailFastIsEnabled() {
final String name = "my-config";
final String namespace = "default";
final String path = String.format("/api/v1/namespaces/%s/configmaps/%s", namespace, name);
void locateShouldThrowExceptionOnFailureWhenFailFastIsEnabled() {
String name = "my-config";
String namespace = "default";
String path = String.format("/api/v1/namespaces/%s/configmaps/%s", namespace, name);
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
@@ -59,10 +66,10 @@ public class Fabric8ConfigMapPropertySourceLocatorTests {
}
@Test
public void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() {
final String name = "my-config";
final String namespace = "default";
final String path = String.format("/api/v1/namespaces/%s/configmaps/%s", namespace, name);
void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() {
String name = "my-config";
String namespace = "default";
String path = String.format("/api/v1/namespaces/%s/configmaps/%s", namespace, name);
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
@@ -77,4 +84,20 @@ public class Fabric8ConfigMapPropertySourceLocatorTests {
assertThatNoException().isThrownBy(() -> locator.locate(new MockEnvironment()));
}
@Test
void constructorWithoutClientNamespaceMustFail() {
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties();
configMapConfigProperties.setName("name");
configMapConfigProperties.setNamespace(null);
configMapConfigProperties.setFailFast(false);
Mockito.when(client.getNamespace()).thenReturn(null);
Fabric8ConfigMapPropertySourceLocator source = new Fabric8ConfigMapPropertySourceLocator(client,
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("name", null, false, "prefix", false);
assertThatThrownBy(() -> source.getMapPropertySource(normalizedSource, new MockEnvironment()))
.isInstanceOf(NamespaceResolutionFailedException.class);
}
}

View File

@@ -23,7 +23,8 @@ import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.cloud.kubernetes.commons.config.NamespaceResolutionFailedException;
import org.springframework.cloud.kubernetes.commons.config.NamedConfigMapNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSource;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
@@ -49,9 +50,10 @@ class Fabric8ConfigMapPropertySourceTests {
final String path = String.format("/api/v1/namespaces/%s/configmaps/%s", namespace, name);
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
assertThatThrownBy(() -> new Fabric8ConfigMapPropertySource(mockClient, name, namespace, new MockEnvironment(),
"", false, true)).isInstanceOf(IllegalStateException.class).hasMessage(
"Unable to read ConfigMap with name '" + name + "' in namespace '" + namespace + "'");
NormalizedSource source = new NamedConfigMapNormalizedSource(name, namespace, true, "default", true);
Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "default", new MockEnvironment());
assertThatThrownBy(() -> new Fabric8ConfigMapPropertySource(context)).isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read ConfigMap with name '" + name + "' in namespace '" + namespace + "'");
}
@Test
@@ -61,33 +63,27 @@ class Fabric8ConfigMapPropertySourceTests {
final String path = String.format("/api/v1/namespaces/%s/configmaps/%s", namespace, name);
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
assertThatNoException().isThrownBy(() -> new Fabric8ConfigMapPropertySource(mockClient, name, namespace,
new MockEnvironment(), "", false, false));
}
@Test
void constructorWithoutClientNamespaceMustFail() {
Mockito.when(client.getNamespace()).thenReturn(null);
assertThatThrownBy(() -> new Fabric8ConfigMapPropertySource(client, "configmap", null, new MockEnvironment(),
"", false, false)).isInstanceOf(NamespaceResolutionFailedException.class);
NormalizedSource source = new NamedConfigMapNormalizedSource(name, namespace, false, "", false);
Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "", new MockEnvironment());
assertThatNoException().isThrownBy(() -> new Fabric8ConfigMapPropertySource(context));
}
@Test
void constructorWithClientNamespaceMustNotFail() {
Mockito.when(client.getNamespace()).thenReturn("namespace");
assertThat(
new Fabric8ConfigMapPropertySource(client, "configmap", null, new MockEnvironment(), "", false, false))
.isNotNull();
NormalizedSource source = new NamedConfigMapNormalizedSource("configmap", null, false, "", false);
Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "", new MockEnvironment());
assertThat(new Fabric8ConfigMapPropertySource(context)).isNotNull();
}
@Test
void constructorWithNamespaceMustNotFail() {
Mockito.when(client.getNamespace()).thenReturn(null);
assertThat(new Fabric8ConfigMapPropertySource(client, "configmap", "namespace", new MockEnvironment(), "",
false, false)).isNotNull();
NormalizedSource source = new NamedConfigMapNormalizedSource("configMap", null, false, "", true);
Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "", new MockEnvironment());
assertThat(new Fabric8ConfigMapPropertySource(context)).isNotNull();
}
}

View File

@@ -56,8 +56,7 @@ public class Fabric8SecretsPropertySourceLocatorTests {
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
assertThatThrownBy(() -> locator.locate(new MockEnvironment())).isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read Secret with name '" + name + "' or labels [{}] in namespace '" + namespace
+ "'");
.hasMessage("Unable to read Secret with name '" + name + "' in namespace '" + namespace + "'");
}
@Test

View File

@@ -17,17 +17,17 @@
package org.springframework.cloud.kubernetes.fabric8.config;
import java.util.Collections;
import java.util.Map;
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.cloud.kubernetes.commons.config.NamespaceResolutionFailedException;
import org.springframework.cloud.kubernetes.commons.config.LabeledSecretNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.NamedSecretNormalizedSource;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -43,52 +43,58 @@ class Fabric8SecretsPropertySourceMockTests {
private static KubernetesClient client;
private final DefaultKubernetesClient mockClient = Mockito.mock(DefaultKubernetesClient.class);
@Test
void constructorShouldThrowExceptionOnFailureWhenFailFastIsEnabled() {
void namedStrategyShouldThrowExceptionOnFailureWhenFailFastIsEnabled() {
final String name = "my-config";
final String namespace = "default";
final String path = String.format("/api/v1/namespaces/%s/secrets/%s", namespace, name);
NamedSecretNormalizedSource named = new NamedSecretNormalizedSource(name, namespace, true);
Fabric8ConfigContext context = new Fabric8ConfigContext(client, named, "default", new MockEnvironment());
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
assertThatThrownBy(
() -> new Fabric8SecretsPropertySource(client, name, namespace, Collections.emptyMap(), true))
.isInstanceOf(IllegalStateException.class).hasMessage("Unable to read Secret with name '" + name
+ "' or labels [{}] in namespace '" + namespace + "'");
assertThatThrownBy(() -> new Fabric8SecretsPropertySource(context)).isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read Secret with name '" + name + "' in namespace '" + namespace + "'");
}
@Test
void constructorShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() {
void labeledStrategyShouldThrowExceptionOnFailureWhenFailFastIsEnabled() {
final String namespace = "default";
final Map<String, String> labels = Collections.singletonMap("a", "b");
final String path = String.format("/api/v1/namespaces/%s/secrets?labelSelector=", namespace) + "a%3Db";
LabeledSecretNormalizedSource labeled = new LabeledSecretNormalizedSource(namespace, labels, true);
Fabric8ConfigContext context = new Fabric8ConfigContext(client, labeled, "default", new MockEnvironment());
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
assertThatThrownBy(() -> new Fabric8SecretsPropertySource(context)).isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read Secret with labels [" + labels + "] in namespace '" + namespace + "'");
}
@Test
void namedStrategyShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() {
final String name = "my-config";
final String namespace = "default";
final String path = String.format("/api/v1/namespaces/%s/secrets/%s", namespace, name);
NamedSecretNormalizedSource named = new NamedSecretNormalizedSource(name, namespace, false);
Fabric8ConfigContext context = new Fabric8ConfigContext(client, named, "default", new MockEnvironment());
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
assertThatNoException().isThrownBy(
() -> new Fabric8SecretsPropertySource(client, name, namespace, Collections.emptyMap(), false));
assertThatNoException().isThrownBy(() -> new Fabric8SecretsPropertySource(context));
}
@Test
void constructorWithoutClientNamespaceMustFail() {
Mockito.when(mockClient.getNamespace()).thenReturn(null);
assertThatThrownBy(
() -> new Fabric8SecretsPropertySource(mockClient, "my-secret", null, Collections.emptyMap(), false))
.isInstanceOf(NamespaceResolutionFailedException.class);
}
void labeledStrategyShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() {
final String namespace = "default";
final Map<String, String> labels = Collections.singletonMap("a", "b");
final String path = String.format("/api/v1/namespaces/%s/secrets?labelSelector=", namespace) + "a%3Db";
@Test
void constructorWithClientNamespaceMustNotFail() {
Mockito.when(mockClient.getNamespace()).thenReturn("namespace");
assertThat(new Fabric8SecretsPropertySource(mockClient, "my-secret", null, Collections.emptyMap(), false))
.isNotNull();
}
LabeledSecretNormalizedSource labeled = new LabeledSecretNormalizedSource(namespace, labels, false);
Fabric8ConfigContext context = new Fabric8ConfigContext(client, labeled, "default", new MockEnvironment());
@Test
void constructorWithNamespaceMustNotFail() {
Mockito.when(mockClient.getNamespace()).thenReturn(null);
assertThat(new Fabric8SecretsPropertySource(mockClient, "my-secret", "ns", Collections.emptyMap(), false))
.isNotNull();
mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once();
assertThatNoException().isThrownBy(() -> new Fabric8SecretsPropertySource(context));
}
}

View File

@@ -0,0 +1,203 @@
/*
* Copyright 2012-2022 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.fabric8.config;
import java.util.Base64;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.api.model.SecretBuilder;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.kubernetes.commons.config.LabeledSecretNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.SecretsPropertySource;
import org.springframework.cloud.kubernetes.commons.config.SourceData;
import org.springframework.mock.env.MockEnvironment;
/**
* Tests only for the happy-path scenarios. All others are tested elsewhere.
*
* @author wind57
*/
@EnableKubernetesMockClient(crud = true, https = false)
class LabeledSecretContextToSourceDataProviderTests {
private static final String NAMESPACE = "default";
private static final Map<String, String> LABELS = new LinkedHashMap<>();
private static final Map<String, String> RED_LABEL = Map.of("color", "red");
private static final Map<String, String> PINK_LABEL = Map.of("color", "pink");
private static final Map<String, String> BLUE_LABEL = Map.of("color", "blue");
private static KubernetesClient mockClient;
static {
LABELS.put("label2", "value2");
LABELS.put("label1", "value1");
}
@BeforeAll
static void beforeAll() {
// Configure the kubernetes master url to point to the mock server
System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, mockClient.getConfiguration().getMasterUrl());
System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true");
System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false");
System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false");
System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, NAMESPACE);
System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true");
}
@AfterEach
void afterEach() {
mockClient.secrets().inNamespace(NAMESPACE).delete();
}
/**
* we have a single secret deployed. it has two labels and these match against our
* queries.
*/
@Test
void singleSecretMatchAgainstLabels() {
Secret secret = new SecretBuilder().withNewMetadata().withName("test-secret").withLabels(LABELS).endMetadata()
.addToData("secretName", Base64.getEncoder().encodeToString("secretValue".getBytes())).build();
mockClient.secrets().inNamespace(NAMESPACE).create(secret);
NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, LABELS, true);
Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE,
new MockEnvironment());
Fabric8ContextToSourceData data = LabeledSecretContextToSourceDataProvider.of(Dummy::sourceName).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals("secrets.test-secret.default", sourceData.sourceName());
Assertions.assertEquals(Map.of("secretName", "secretValue"), sourceData.sourceData());
}
/**
* we have three secret deployed. two of them have labels that match (color=red), one
* does not (color=blue).
*/
@Test
void twoSecretsMatchAgainstLabels() {
Secret redOne = new SecretBuilder().withNewMetadata().withName("red-secret").withLabels(RED_LABEL).endMetadata()
.addToData("colorOne", Base64.getEncoder().encodeToString("really-red".getBytes())).build();
Secret redTwo = new SecretBuilder().withNewMetadata().withName("red-secret-again").withLabels(RED_LABEL)
.endMetadata().addToData("colorTwo", Base64.getEncoder().encodeToString("really-red-again".getBytes()))
.build();
Secret blue = new SecretBuilder().withNewMetadata().withName("blue-secret").withLabels(BLUE_LABEL).endMetadata()
.addToData("color", Base64.getEncoder().encodeToString("blue".getBytes())).build();
mockClient.secrets().inNamespace(NAMESPACE).create(redOne);
mockClient.secrets().inNamespace(NAMESPACE).create(redTwo);
mockClient.secrets().inNamespace(NAMESPACE).create(blue);
NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, RED_LABEL, true);
Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE,
new MockEnvironment());
Fabric8ContextToSourceData data = LabeledSecretContextToSourceDataProvider.of(Dummy::sourceName).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "secrets.red-secret.red-secret-again.default");
Assertions.assertEquals(sourceData.sourceData().size(), 2);
Assertions.assertEquals(sourceData.sourceData().get("colorOne"), "really-red");
Assertions.assertEquals(sourceData.sourceData().get("colorTwo"), "really-red-again");
}
/**
* one secret deployed (pink), does not match our query (blue).
*/
@Test
void secretNoMatch() {
Secret pink = new SecretBuilder().withNewMetadata().withName("pink-secret").withLabels(PINK_LABEL).endMetadata()
.addToData("color", Base64.getEncoder().encodeToString("pink".getBytes())).build();
mockClient.secrets().inNamespace(NAMESPACE).create(pink);
NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, BLUE_LABEL, true);
Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE,
new MockEnvironment());
Fabric8ContextToSourceData data = LabeledSecretContextToSourceDataProvider.of(Dummy::sourceName).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "secrets.color.default");
Assertions.assertEquals(sourceData.sourceData(), Collections.emptyMap());
}
/**
* LabeledSecretContextToSourceDataProvider gets as input a Fabric8ConfigContext. This
* context has a namespace as well as a NormalizedSource, that has a namespace too. It
* is easy to get confused in code on which namespace to use. This test makes sure
* that we use the proper one.
*/
@Test
void namespaceMatch() {
Secret secret = new SecretBuilder().withNewMetadata().withName("test-secret").withLabels(LABELS).endMetadata()
.addToData("secretName", Base64.getEncoder().encodeToString("secretValue".getBytes())).build();
mockClient.secrets().inNamespace(NAMESPACE).create(secret);
// different namespace
NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE + "nope", LABELS, true);
Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE,
new MockEnvironment());
Fabric8ContextToSourceData data = LabeledSecretContextToSourceDataProvider.of(Dummy::sourceName).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals("secrets.test-secret.default", sourceData.sourceName());
Assertions.assertEquals(Map.of("secretName", "secretValue"), sourceData.sourceData());
}
// needed only to allow access to the super methods
private final static class Dummy extends SecretsPropertySource {
private Dummy() {
super(SourceData.emptyRecord("dummy-name"));
}
private static String sourceName(String name, String namespace) {
return getSourceName(name, namespace);
}
}
}

View File

@@ -0,0 +1,302 @@
/*
* Copyright 2012-2022 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.fabric8.config;
import java.util.Collections;
import java.util.Map;
import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.kubernetes.commons.config.ConfigMapPrefixContext;
import org.springframework.cloud.kubernetes.commons.config.ConfigMapPropertySource;
import org.springframework.cloud.kubernetes.commons.config.NamedConfigMapNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.SourceData;
import org.springframework.core.env.Environment;
import org.springframework.mock.env.MockEnvironment;
/**
* Tests only for the happy-path scenarios. All others are tested elsewhere.
*
* @author wind57
*/
@EnableKubernetesMockClient(crud = true, https = false)
class NamedConfigMapContextToSourceDataProviderTests {
private static final String NAMESPACE = "default";
private static KubernetesClient mockClient;
@BeforeAll
static void beforeAll() {
// Configure the kubernetes master url to point to the mock server
System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, mockClient.getConfiguration().getMasterUrl());
System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true");
System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false");
System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false");
System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, NAMESPACE);
System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true");
}
@AfterEach
void afterEach() {
mockClient.configMaps().inNamespace(NAMESPACE).delete();
}
/**
* we have a single config map deployed. it does not match our query.
*/
@Test
void noMatch() {
ConfigMap configMap = new ConfigMapBuilder().withNewMetadata().withName("red").endMetadata()
.addToData("color", "really-red").build();
mockClient.configMaps().inNamespace(NAMESPACE).create(configMap);
NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("blue", NAMESPACE, true, "", false);
Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE,
new MockEnvironment());
Fabric8ContextToSourceData data = NamedConfigMapContextToSourceDataProvider
.of(Dummy::processEntries, Dummy::sourceName, Dummy::prefix).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "configmap.blue.default");
Assertions.assertEquals(sourceData.sourceData(), Collections.emptyMap());
}
/**
* we have a single config map deployed. it matches our query.
*/
@Test
void match() {
ConfigMap configMap = new ConfigMapBuilder().withNewMetadata().withName("red").endMetadata()
.addToData("color", "really-red").build();
mockClient.configMaps().inNamespace(NAMESPACE).create(configMap);
NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("red", NAMESPACE, true, "", false);
Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE,
new MockEnvironment());
Fabric8ContextToSourceData data = NamedConfigMapContextToSourceDataProvider
.of(Dummy::processEntries, Dummy::sourceName, Dummy::prefix).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "configmap.red.default");
Assertions.assertEquals(sourceData.sourceData(), Collections.singletonMap("color", "really-red"));
}
/**
* we have two config maps deployed. one matches the query name. the other matches the
* active profile + name, thus is taken also.
*/
@Test
void matchIncludeSingleProfile() {
ConfigMap red = new ConfigMapBuilder().withNewMetadata().withName("red").endMetadata()
.addToData("color", "really-red").build();
ConfigMap redWithProfile = new ConfigMapBuilder().withNewMetadata().withName("red-with-profile").endMetadata()
.addToData("taste", "mango").build();
mockClient.configMaps().inNamespace(NAMESPACE).create(red);
mockClient.configMaps().inNamespace(NAMESPACE).create(redWithProfile);
// add one more profile and specify that we want profile based config maps
MockEnvironment env = new MockEnvironment();
env.setActiveProfiles("with-profile");
NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("red", NAMESPACE, true, "", true);
Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env);
Fabric8ContextToSourceData data = NamedConfigMapContextToSourceDataProvider
.of(Dummy::processEntries, Dummy::sourceName, Dummy::prefix).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "configmap.red.red-with-profile.default");
Assertions.assertEquals(sourceData.sourceData().size(), 2);
Assertions.assertEquals(sourceData.sourceData().get("color"), "really-red");
Assertions.assertEquals(sourceData.sourceData().get("taste"), "mango");
}
/**
* we have two config maps deployed. one matches the query name. the other matches the
* active profile + name, thus is taken also. This takes into consideration the
* prefix, that we explicitly specify. Notice that prefix works for profile based
* config maps as well.
*/
@Test
void matchIncludeSingleProfileWithPrefix() {
ConfigMap red = new ConfigMapBuilder().withNewMetadata().withName("red").endMetadata()
.addToData("color", "really-red").build();
ConfigMap redWithProfile = new ConfigMapBuilder().withNewMetadata().withName("red-with-profile").endMetadata()
.addToData("taste", "mango").build();
mockClient.configMaps().inNamespace(NAMESPACE).create(red);
mockClient.configMaps().inNamespace(NAMESPACE).create(redWithProfile);
// add one more profile and specify that we want profile based config maps
// also append prefix
MockEnvironment env = new MockEnvironment();
env.setActiveProfiles("with-profile");
NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("red", NAMESPACE, true, "some", true);
Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env);
Fabric8ContextToSourceData data = NamedConfigMapContextToSourceDataProvider
.of(Dummy::processEntries, Dummy::sourceName, Dummy::prefix).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "configmap.red.red-with-profile.default");
Assertions.assertEquals(sourceData.sourceData().size(), 2);
Assertions.assertEquals(sourceData.sourceData().get("some.color"), "really-red");
Assertions.assertEquals(sourceData.sourceData().get("some.taste"), "mango");
}
/**
* we have three config maps deployed. one matches the query name. the other two match
* the active profile + name, thus are taken also. This takes into consideration the
* prefix, that we explicitly specify. Notice that prefix works for profile based
* config maps as well.
*/
@Test
void matchIncludeTwoProfilesWithPrefix() {
ConfigMap red = new ConfigMapBuilder().withNewMetadata().withName("red").endMetadata()
.addToData("color", "really-red").build();
ConfigMap redWithTaste = new ConfigMapBuilder().withNewMetadata().withName("red-with-taste").endMetadata()
.addToData("taste", "mango").build();
ConfigMap redWithShape = new ConfigMapBuilder().withNewMetadata().withName("red-with-shape").endMetadata()
.addToData("shape", "round").build();
mockClient.configMaps().inNamespace(NAMESPACE).create(red);
mockClient.configMaps().inNamespace(NAMESPACE).create(redWithTaste);
mockClient.configMaps().inNamespace(NAMESPACE).create(redWithShape);
// add one more profile and specify that we want profile based config maps
// also append prefix
MockEnvironment env = new MockEnvironment();
env.setActiveProfiles("with-taste", "with-shape");
NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("red", NAMESPACE, true, "some", true);
Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env);
Fabric8ContextToSourceData data = NamedConfigMapContextToSourceDataProvider
.of(Dummy::processEntries, Dummy::sourceName, Dummy::prefix).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "configmap.red.red-with-taste.red-with-shape.default");
Assertions.assertEquals(sourceData.sourceData().size(), 3);
Assertions.assertEquals(sourceData.sourceData().get("some.color"), "really-red");
Assertions.assertEquals(sourceData.sourceData().get("some.taste"), "mango");
Assertions.assertEquals(sourceData.sourceData().get("some.shape"), "round");
}
// this test makes sure that even if NormalizedSource has no name (which is a valid
// case for config maps),
// it will default to "application" and such a config map will be read.
@Test
void matchWithoutName() {
ConfigMap configMap = new ConfigMapBuilder().withNewMetadata().withName("application").endMetadata()
.addToData("color", "red").build();
mockClient.configMaps().inNamespace(NAMESPACE).create(configMap);
NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource(null, NAMESPACE, true, "", false);
Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE,
new MockEnvironment());
Fabric8ContextToSourceData data = NamedConfigMapContextToSourceDataProvider
.of(Dummy::processEntries, Dummy::sourceName, Dummy::prefix).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "configmap.application.default");
Assertions.assertEquals(sourceData.sourceData(), Collections.singletonMap("color", "red"));
}
/**
* NamedSecretContextToSourceDataProvider gets as input a Fabric8ConfigContext. This
* context has a namespace as well as a NormalizedSource, that has a namespace too. It
* is easy to get confused in code on which namespace to use. This test makes sure
* that we use the proper one.
*/
@Test
void namespaceMatch() {
ConfigMap configMap = new ConfigMapBuilder().withNewMetadata().withName("red").endMetadata()
.addToData("color", "really-red").build();
mockClient.configMaps().inNamespace(NAMESPACE).create(configMap);
// different namespace
NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("red", NAMESPACE + "nope", true, "",
false);
Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE,
new MockEnvironment());
Fabric8ContextToSourceData data = NamedConfigMapContextToSourceDataProvider
.of(Dummy::processEntries, Dummy::sourceName, Dummy::prefix).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "configmap.red.default");
Assertions.assertEquals(sourceData.sourceData(), Collections.singletonMap("color", "really-red"));
}
// needed only to allow access to the super methods
private static final class Dummy extends ConfigMapPropertySource {
private Dummy() {
super(SourceData.emptyRecord("dummy-name"));
}
private static String sourceName(String name, String namespace) {
return getSourceName(name, namespace);
}
private static Map<String, Object> processEntries(Map<String, String> map, Environment environment) {
return processAllEntries(map, environment);
}
private static SourceData prefix(ConfigMapPrefixContext context) {
return withPrefix(context);
}
}
}

View File

@@ -0,0 +1,186 @@
/*
* Copyright 2012-2022 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.fabric8.config;
import java.util.Base64;
import java.util.Collections;
import java.util.Map;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.api.model.SecretBuilder;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.kubernetes.commons.config.NamedSecretNormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.NormalizedSource;
import org.springframework.cloud.kubernetes.commons.config.SecretsPropertySource;
import org.springframework.cloud.kubernetes.commons.config.SourceData;
import org.springframework.mock.env.MockEnvironment;
/**
* Tests only for the happy-path scenarios. All others are tested elsewhere.
*
* @author wind57
*/
@EnableKubernetesMockClient(crud = true, https = false)
class NamedSecretContextToSourceDataProviderTests {
private static final String NAMESPACE = "default";
private static KubernetesClient mockClient;
@BeforeAll
static void beforeAll() {
// Configure the kubernetes master url to point to the mock server
System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, mockClient.getConfiguration().getMasterUrl());
System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true");
System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false");
System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false");
System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, NAMESPACE);
System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true");
}
@AfterEach
void afterEach() {
mockClient.secrets().inNamespace(NAMESPACE).delete();
}
/**
* we have a single secret deployed. it matched the name in our queries
*/
@Test
void singleSecretMatchAgainstLabels() {
Secret secret = new SecretBuilder().withNewMetadata().withName("red").endMetadata()
.addToData("color", Base64.getEncoder().encodeToString("really-red".getBytes())).build();
mockClient.secrets().inNamespace(NAMESPACE).create(secret);
NormalizedSource normalizedSource = new NamedSecretNormalizedSource("red", NAMESPACE, true);
Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE,
new MockEnvironment());
Fabric8ContextToSourceData data = NamedSecretContextToSourceDataProvider.of(Dummy::sourceName).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "secrets.red.default");
Assertions.assertEquals(sourceData.sourceData(), Map.of("color", "really-red"));
}
/**
* we have three secret deployed. one of them has a name that matches (red), the other
* two have different names, thus no match.
*/
@Test
void twoSecretMatchAgainstLabels() {
Secret red = new SecretBuilder().withNewMetadata().withName("red").endMetadata()
.addToData("color", Base64.getEncoder().encodeToString("really-red".getBytes())).build();
Secret blue = new SecretBuilder().withNewMetadata().withName("blue").endMetadata()
.addToData("color", Base64.getEncoder().encodeToString("really-blue".getBytes())).build();
Secret yellow = new SecretBuilder().withNewMetadata().withName("yellow").endMetadata()
.addToData("color", Base64.getEncoder().encodeToString("really-yeallow".getBytes())).build();
mockClient.secrets().inNamespace(NAMESPACE).create(red);
mockClient.secrets().inNamespace(NAMESPACE).create(blue);
mockClient.secrets().inNamespace(NAMESPACE).create(yellow);
NormalizedSource normalizedSource = new NamedSecretNormalizedSource("red", NAMESPACE, true);
Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE,
new MockEnvironment());
Fabric8ContextToSourceData data = NamedSecretContextToSourceDataProvider.of(Dummy::sourceName).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "secrets.red.default");
Assertions.assertEquals(sourceData.sourceData().size(), 1);
Assertions.assertEquals(sourceData.sourceData().get("color"), "really-red");
}
/**
* one secret deployed (pink), does not match our query (blue).
*/
@Test
void testSecretNoMatch() {
Secret pink = new SecretBuilder().withNewMetadata().withName("pink").endMetadata()
.addToData("color", Base64.getEncoder().encodeToString("pink".getBytes())).build();
mockClient.secrets().inNamespace(NAMESPACE).create(pink);
NormalizedSource normalizedSource = new NamedSecretNormalizedSource("blue", NAMESPACE, true);
Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE,
new MockEnvironment());
Fabric8ContextToSourceData data = NamedSecretContextToSourceDataProvider.of(Dummy::sourceName).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "secrets.blue.default");
Assertions.assertEquals(sourceData.sourceData(), Collections.emptyMap());
}
/**
* NamedSecretContextToSourceDataProvider gets as input a Fabric8ConfigContext. This
* context has a namespace as well as a NormalizedSource, that has a namespace too. It
* is easy to get confused in code on which namespace to use. This test makes sure
* that we use the proper one.
*/
@Test
void namespaceMatch() {
Secret secret = new SecretBuilder().withNewMetadata().withName("red").endMetadata()
.addToData("color", Base64.getEncoder().encodeToString("really-red".getBytes())).build();
mockClient.secrets().inNamespace(NAMESPACE).create(secret);
// different namespace
NormalizedSource normalizedSource = new NamedSecretNormalizedSource("red", NAMESPACE + "nope", true);
Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE,
new MockEnvironment());
Fabric8ContextToSourceData data = NamedSecretContextToSourceDataProvider.of(Dummy::sourceName).get();
SourceData sourceData = data.apply(context);
Assertions.assertEquals(sourceData.sourceName(), "secrets.red.default");
Assertions.assertEquals(sourceData.sourceData(), Map.of("color", "really-red"));
}
// needed only to allow access to the super methods
private static final class Dummy extends SecretsPropertySource {
private Dummy() {
super(SourceData.emptyRecord("dummy-name"));
}
private static String sourceName(String name, String namespace) {
return getSourceName(name, namespace);
}
}
}

View File

@@ -38,12 +38,19 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* we call Fabric8ConfigMapPropertySourceLocator::locate directly, thus no need for
* bootstrap phase to kick in. As such two flags that might look a bit un-expected:
* "spring.cloud.kubernetes.config.enabled=false"
* "spring.cloud.kubernetes.secrets.enabled=false"
*
* @author Isik Erhan
* @author wind57
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
properties = { "spring.cloud.kubernetes.client.namespace=default",
"spring.cloud.kubernetes.config.fail-fast=true", "spring.cloud.kubernetes.config.retry.enabled=false",
"spring.main.cloud-platform=KUBERNETES" },
"spring.main.cloud-platform=KUBERNETES", "spring.cloud.kubernetes.config.enabled=false",
"spring.cloud.kubernetes.secrets.enabled=false" },
classes = Application.class)
@EnableKubernetesMockClient
class ConfigFailFastEnabledButRetryDisabled {

View File

@@ -85,7 +85,7 @@ class SecretsFailFastEnabledButRetryDisabled {
assertThat(context.containsBean("kubernetesSecretsRetryInterceptor")).isFalse();
assertThatThrownBy(() -> propertySourceLocator.locate(new MockEnvironment()))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read Secret with name 'my-secret' or labels [{}] in namespace 'default'");
.hasMessage("Unable to read Secret with name 'my-secret' in namespace 'default'");
// verify that propertySourceLocator.locate is called only once
verify(propertySourceLocator, times(1)).locate(any());

View File

@@ -92,7 +92,7 @@ class SecretsRetryDisabledButConfigRetryEnabled {
assertThat(context.containsBean("kubernetesSecretsRetryInterceptor")).isTrue();
assertThatThrownBy(() -> propertySourceLocator.locate(new MockEnvironment()))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read Secret with name 'my-secret' or labels [{}] in namespace 'default'");
.hasMessage("Unable to read Secret with name 'my-secret' in namespace 'default'");
// verify that propertySourceLocator.locate is called only once
verify(propertySourceLocator, times(1)).locate(any());

View File

@@ -130,7 +130,7 @@ class SecretsRetryEnabled {
assertThatThrownBy(() -> propertySourceLocator.locate(new MockEnvironment()))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Unable to read Secret with name 'my-secret' or labels [{}] in namespace 'default'");
.hasMessage("Unable to read Secret with name 'my-secret' in namespace 'default'");
// verify retried 5 times until failure
verify(propertySourceLocator, times(5)).locate(any());

View File

@@ -15,4 +15,6 @@
<suppress files=".*ConfigurationWatcherApplication\.java" checks="HideUtilityClassConstructor"/>
<suppress files=".*DiscoveryServerApplication\.java" checks="HideUtilityClassConstructor"/>
<suppress files=".*KubernetesConfigServerApplication\.java" checks="HideUtilityClassConstructor"/>
<suppress files=".*NormalizedSource\.java" checks="WhitespaceAfter" />
<suppress files=".*Fabric8ConfigContext\.java" checks="WhitespaceAround" />
</suppressions>