Major refactor of configmaps and secrets implementation (#917)
This commit is contained in:
@@ -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) {
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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(","));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -40,8 +40,6 @@ public abstract class AbstractConfigProperties {
|
||||
|
||||
protected RetryProperties retry = new RetryProperties();
|
||||
|
||||
public abstract String getConfigurationTarget();
|
||||
|
||||
public boolean isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user