diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigContext.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigContext.java new file mode 100644 index 00000000..6a7fa922 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigContext.java @@ -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) { +} diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySource.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySource.java index 506db53f..46cd1c23 100644 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySource.java +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySource.java @@ -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 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 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 names = new HashSet<>(); - names.add(name); - if (environment != null && includeProfileSpecificSources) { - for (String activeProfile : environment.getActiveProfiles()) { - names.add(name + "-" + activeProfile); - } - } - Map 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 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(); } } diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocator.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocator.java index 68f36aca..a9aff272 100644 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocator.java +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocator.java @@ -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); } } diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigUtils.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigUtils.java index cc71b31a..9bee82d0 100644 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigUtils.java +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigUtils.java @@ -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 dataFromSecret(V1Secret secret, String namespace) { + LOG.debug("reading secret with name : " + secret.getMetadata().getName() + " in namespace : " + namespace); + Map data = secret.getData(); + + Map 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(); + } + } diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientContextToSourceData.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientContextToSourceData.java new file mode 100644 index 00000000..070a8b84 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientContextToSourceData.java @@ -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 { + +} diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySource.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySource.java index c041f926..fa608c96 100644 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySource.java +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySource.java @@ -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 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 getSourceData(CoreV1Api api, String name, String namespace, - Map labels, boolean failFast) { - Map result = new HashMap<>(); + private static final EnumMap 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 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 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 result) { - Map 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(); } } diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocator.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocator.java index d39d7659..a4b93bb0 100644 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocator.java +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocator.java @@ -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); } } diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/LabeledSecretContextToSourceDataProvider.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/LabeledSecretContextToSourceDataProvider.java new file mode 100644 index 00000000..e5269d8e --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/LabeledSecretContextToSourceDataProvider.java @@ -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 { + + private static final Log LOG = LogFactory.getLog(LabeledSecretContextToSourceDataProvider.class); + + private final BiFunction sourceNameMapper; + + private LabeledSecretContextToSourceDataProvider(BiFunction sourceNameFunction) { + this.sourceNameMapper = Objects.requireNonNull(sourceNameFunction); + } + + static LabeledSecretContextToSourceDataProvider of(BiFunction 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 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 result = new HashMap<>(); + LabeledSecretNormalizedSource source = (LabeledSecretNormalizedSource) context.normalizedSource(); + Map 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 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 labels) { + return labels.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining(",")); + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/NamedConfigMapContextToSourceDataProvider.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/NamedConfigMapContextToSourceDataProvider.java new file mode 100644 index 00000000..33722890 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/NamedConfigMapContextToSourceDataProvider.java @@ -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 { + + private static final Log LOG = LogFactory.getLog(NamedConfigMapContextToSourceDataProvider.class); + + private final BiFunction, Environment, Map> entriesProcessor; + + private final BiFunction sourceNameMapper; + + private final Function withPrefix; + + private NamedConfigMapContextToSourceDataProvider( + BiFunction, Environment, Map> entriesProcessor, + BiFunction sourceNameMapper, + Function withPrefix) { + this.entriesProcessor = Objects.requireNonNull(entriesProcessor); + this.sourceNameMapper = Objects.requireNonNull(sourceNameMapper); + this.withPrefix = Objects.requireNonNull(withPrefix); + } + + static NamedConfigMapContextToSourceDataProvider of( + BiFunction, Environment, Map> entriesProcessor, + BiFunction sourceNameMapper, + Function 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 propertySourceNames = new LinkedHashSet<>(); + propertySourceNames.add(configMapName); + + Map result = new HashMap<>(); + try { + Set 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 appName(Environment environment, NormalizedSource normalizedSource) { + return () -> getApplicationName(environment, normalizedSource.name().orElse(null), normalizedSource.target()); + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/NamedSecretContextToSourceDataProvider.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/NamedSecretContextToSourceDataProvider.java new file mode 100644 index 00000000..3211b5ef --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/NamedSecretContextToSourceDataProvider.java @@ -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 { + + private static final Log LOG = LogFactory.getLog(NamedSecretContextToSourceDataProvider.class); + + private final BiFunction sourceNameMapper; + + private NamedSecretContextToSourceDataProvider(BiFunction sourceNameFunction) { + this.sourceNameMapper = Objects.requireNonNull(sourceNameFunction); + } + + static NamedSecretContextToSourceDataProvider of(BiFunction sourceNameFunction) { + return new NamedSecretContextToSourceDataProvider(sourceNameFunction); + } + + @Override + public KubernetesClientContextToSourceData get() { + return context -> { + + NamedSecretNormalizedSource source = (NamedSecretNormalizedSource) context.normalizedSource(); + + Map 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 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); + + }; + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocatorTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocatorTests.java index b2305a91..4f636df0 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocatorTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocatorTests.java @@ -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 diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceTests.java index 7bbccd71..b7c42c8c 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceTests.java @@ -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"))); } diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload/KubernetesClientConfigReloadAutoConfigurationTest.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigReloadAutoConfigurationTest.java similarity index 98% rename from spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload/KubernetesClientConfigReloadAutoConfigurationTest.java rename to spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigReloadAutoConfigurationTest.java index 48743847..944fc1bd 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload/KubernetesClientConfigReloadAutoConfigurationTest.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigReloadAutoConfigurationTest.java @@ -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; diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocatorTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocatorTests.java index 28abe97c..f0742969 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocatorTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocatorTests.java @@ -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 diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceTests.java index 0ad304ab..b0cbfd96 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceTests.java @@ -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 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); - } - } diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/LabeledSecretContextToSourceDataProviderTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/LabeledSecretContextToSourceDataProviderTests.java new file mode 100644 index 00000000..6e83c1ac --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/LabeledSecretContextToSourceDataProviderTests.java @@ -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 LABELS = new LinkedHashMap<>(); + + private static final Map 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); + } + + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/NamedConfigMapContextToSourceDataProviderTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/NamedConfigMapContextToSourceDataProviderTests.java new file mode 100644 index 00000000..a527c1d0 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/NamedConfigMapContextToSourceDataProviderTests.java @@ -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 processEntries(Map map, Environment environment) { + return processAllEntries(map, environment); + } + + private static SourceData prefix(ConfigMapPrefixContext context) { + return withPrefix(context); + } + + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/NamedSecretContextToSourceDataProviderTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/NamedSecretContextToSourceDataProviderTests.java new file mode 100644 index 00000000..8efc74fe --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/NamedSecretContextToSourceDataProviderTests.java @@ -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); + } + + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/configmap_retry/ConfigFailFastEnabledButRetryDisabled.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/configmap_retry/ConfigFailFastEnabledButRetryDisabled.java index a5abe60c..e40d2c96 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/configmap_retry/ConfigFailFastEnabledButRetryDisabled.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/configmap_retry/ConfigFailFastEnabledButRetryDisabled.java @@ -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()); diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/configmap_retry/ConfigRetryDisabledButSecretsRetryEnabled.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/configmap_retry/ConfigRetryDisabledButSecretsRetryEnabled.java index 90c37aab..497e3f86 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/configmap_retry/ConfigRetryDisabledButSecretsRetryEnabled.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/configmap_retry/ConfigRetryDisabledButSecretsRetryEnabled.java @@ -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()); diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/configmap_retry/ConfigRetryEnabled.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/configmap_retry/ConfigRetryEnabled.java index 9d475a5a..7bfa4e31 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/configmap_retry/ConfigRetryEnabled.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/configmap_retry/ConfigRetryEnabled.java @@ -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()); diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/secrets_retry/SecretsFailFastEnabledButRetryDisabled.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/secrets_retry/SecretsFailFastEnabledButRetryDisabled.java index e2980ed0..929a4ea3 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/secrets_retry/SecretsFailFastEnabledButRetryDisabled.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/secrets_retry/SecretsFailFastEnabledButRetryDisabled.java @@ -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()); diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/secrets_retry/SecretsRetryDisabledButConfigRetryEnabled.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/secrets_retry/SecretsRetryDisabledButConfigRetryEnabled.java index 083d29a1..d1bafcbe 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/secrets_retry/SecretsRetryDisabledButConfigRetryEnabled.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/secrets_retry/SecretsRetryDisabledButConfigRetryEnabled.java @@ -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()); diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/secrets_retry/SecretsRetryEnabled.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/secrets_retry/SecretsRetryEnabled.java index a6f160c1..3431ff1e 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/secrets_retry/SecretsRetryEnabled.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/secrets_retry/SecretsRetryEnabled.java @@ -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()); diff --git a/spring-cloud-kubernetes-commons/pom.xml b/spring-cloud-kubernetes-commons/pom.xml index c20f7fd9..947bfc1d 100644 --- a/spring-cloud-kubernetes-commons/pom.xml +++ b/spring-cloud-kubernetes-commons/pom.xml @@ -10,8 +10,20 @@ 4.0.0 spring-cloud-kubernetes-commons + + + + org.apache.maven.plugins + maven-compiler-plugin + + 17 + 17 + + + + - + org.springframework.boot spring-boot-autoconfigure diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/AbstractConfigProperties.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/AbstractConfigProperties.java index d2258310..82e2bf59 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/AbstractConfigProperties.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/AbstractConfigProperties.java @@ -40,8 +40,6 @@ public abstract class AbstractConfigProperties { protected RetryProperties retry = new RetryProperties(); - public abstract String getConfigurationTarget(); - public boolean isEnabled() { return this.enabled; } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigProperties.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigProperties.java index 01624f77..021d7379 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigProperties.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigProperties.java @@ -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); - } - - } - } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapPrefixContext.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapPrefixContext.java new file mode 100644 index 00000000..11ad65e2 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapPrefixContext.java @@ -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 data, String prefix, String namespace, + Set propertySourceNames) { +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapPropertySource.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapPropertySource.java index 55d7eff0..fed503c0 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapPropertySource.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapPropertySource.java @@ -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 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 defaultProcessAllEntries(Map 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 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 defaultProcessAllEntries(Map 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 extractProperties(String resourceName, String content, - Environment environment) { + private static Map 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); diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapPropertySourceLocator.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapPropertySourceLocator.java index 38d96efd..92db6b4d 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapPropertySourceLocator.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapPropertySourceLocator.java @@ -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) { diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigUtils.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigUtils.java index 0d507076..2ea64698 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigUtils.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigUtils.java @@ -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); + } + } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/LabeledSecretNormalizedSource.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/LabeledSecretNormalizedSource.java new file mode 100644 index 00000000..20437811 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/LabeledSecretNormalizedSource.java @@ -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 labels; + + public LabeledSecretNormalizedSource(String namespace, Map labels, boolean failFast) { + super(null, namespace, failFast); + this.labels = Collections.unmodifiableMap(Objects.requireNonNull(labels)); + } + + /** + * will return an immutable Map. + */ + public Map 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()); + } + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/NamedConfigMapNormalizedSource.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/NamedConfigMapNormalizedSource.java new file mode 100644 index 00000000..8aeaf0dd --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/NamedConfigMapNormalizedSource.java @@ -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()); + } + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/NamedSecretNormalizedSource.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/NamedSecretNormalizedSource.java new file mode 100644 index 00000000..c045ded7 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/NamedSecretNormalizedSource.java @@ -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()); + } + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/NormalizedSource.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/NormalizedSource.java new file mode 100644 index 00000000..23303d1b --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/NormalizedSource.java @@ -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 namespace() { + return Optional.ofNullable(this.namespace); + } + + // this can return an empty Optional (for a secret based on labels, for example) + public final Optional 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(); + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/NormalizedSourceType.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/NormalizedSourceType.java new file mode 100644 index 00000000..9c09eb36 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/NormalizedSourceType.java @@ -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 + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsConfigProperties.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsConfigProperties.java index 2222b1f7..41257ce1 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsConfigProperties.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsConfigProperties.java @@ -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 determineSources() { + public List determineSources(Environment environment) { if (this.sources.isEmpty()) { - return Collections - .singletonList(new SecretsConfigProperties.NormalizedSource(SecretsConfigProperties.this.name, - SecretsConfigProperties.this.namespace, SecretsConfigProperties.this.labels)); + + List 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 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 defaultLabels) { + private Stream normalize(String defaultName, String defaultNamespace, + Map defaultLabels, boolean failFast, Environment environment) { + + Stream.Builder normalizedSources = Stream.builder(); + String normalizedName = StringUtils.hasLength(this.name) ? this.name : defaultName; String normalizedNamespace = StringUtils.hasLength(this.namespace) ? this.namespace : defaultNamespace; Map 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 labels; - - NormalizedSource(String name, String namespace, Map labels) { - this.name = name; - this.namespace = namespace; - this.labels = labels; - } - - public String getName() { - return this.name; - } - - public String getNamespace() { - return this.namespace; - } - - public Map 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(); } } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySource.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySource.java index 02cefbdc..c4313653 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySource.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySource.java @@ -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 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 data, Map 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 diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java index 7afa8689..46147829 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsPropertySourceLocator.java @@ -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 sources = this.properties.determineSources(); - Set uniqueSources = new HashSet<>(sources); + List sources = this.properties.determineSources(environment); + Set 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) { diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceData.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceData.java new file mode 100644 index 00000000..29b4280e --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceData.java @@ -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 sourceData) { + + public static SourceData emptyRecord(String sourceName) { + return new SourceData(sourceName, Collections.emptyMap()); + } + +} diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigPropertiesTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigPropertiesTests.java index d665fbb4..9da75c03 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigPropertiesTests.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigPropertiesTests.java @@ -47,10 +47,10 @@ public class ConfigMapConfigPropertiesTests { properties.setName("config-map-a"); properties.setNamespace("spring-k8s"); - List sources = properties.determineSources(); + List 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 sources = properties.determineSources(); + List 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 sources = properties.determineSources(); + List 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 sources = properties.determineSources(); + List 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 sources = properties.determineSources(); + List 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 sources = properties.determineSources(); + List 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 sources = properties.determineSources(); + List 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 sources = properties.determineSources(); + List 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()); } } diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapPropertySourceTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapPropertySourceTests.java new file mode 100644 index 00000000..979feb80 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapPropertySourceTests.java @@ -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"); + } + +} diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/LabeledSecretNormalizedSourceTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/LabeledSecretNormalizedSourceTests.java new file mode 100644 index 00000000..ac0d1565 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/LabeledSecretNormalizedSourceTests.java @@ -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 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()); + } + +} diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/NamedConfigMapNormalizedSourceTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/NamedConfigMapNormalizedSourceTests.java new file mode 100644 index 00000000..35d5d8cb --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/NamedConfigMapNormalizedSourceTests.java @@ -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()); + } + +} diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/NamedSecretNormalizedSourceTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/NamedSecretNormalizedSourceTests.java new file mode 100644 index 00000000..3a396867 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/NamedSecretNormalizedSourceTests.java @@ -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()); + } + +} diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SecretsConfigPropertiesTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SecretsConfigPropertiesTests.java new file mode 100644 index 00000000..626d6a0e --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SecretsConfigPropertiesTests.java @@ -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 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"); + } + + /** + *
+	 *     spring:
+	 *        cloud:
+	 *           kubernetes:
+	 *             secrets:
+	 *                sources:
+	 *                   - name : one
+	 *                     labels:
+	 *                       one: "1"
+	 *                   - labels:
+	 *                       two: 2
+	 *                   - labels:
+	 *                       three: 3
+	 * 
+ * + * 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 result = properties.determineSources(new MockEnvironment()); + Assertions.assertEquals(result.size(), 6); + + Set resultAsSet = new LinkedHashSet<>(result); + Assertions.assertEquals(resultAsSet.size(), 5); + + Iterator 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")); + + } + +} diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SourceDataTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SourceDataTests.java new file mode 100644 index 00000000..0b6cf7fb --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SourceDataTests.java @@ -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); + } + +} diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerAutoConfiguration.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerAutoConfiguration.java index 84eea23f..19dc35b6 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerAutoConfiguration.java +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/main/java/org/springframework/cloud/kubernetes/configserver/KubernetesConfigServerAutoConfiguration.java @@ -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 namespaces = namespaceSplitter(properties.getConfigMapNamespaces(), namespace); List 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 namespaces = namespaceSplitter(properties.getSecretsNamespaces(), namespace); List 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; }; } diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/ConfigServerIntegrationTest.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/ConfigServerIntegrationTest.java index 8127e07a..1e3386cf 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/ConfigServerIntegrationTest.java +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/ConfigServerIntegrationTest.java @@ -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( diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryTests.java index 78ce8ab9..76246aa9 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryTests.java +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesEnvironmentRepositoryTests.java @@ -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 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 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"); + } }); } diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8BootstrapConfiguration.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8BootstrapConfiguration.java index 23838db0..6cb0a698 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8BootstrapConfiguration.java +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8BootstrapConfiguration.java @@ -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); } diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigContext.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigContext.java new file mode 100644 index 00000000..c8f032df --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigContext.java @@ -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) { +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySource.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySource.java index 2ede1b5c..54ed82f8 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySource.java +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySource.java @@ -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 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 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 data = getConfigMapData(client, namespace, name); - Map 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 dataWithProfile = getConfigMapData(client, namespace, mapNameWithProfile); - result.putAll(processAllEntries(dataWithProfile, environment)); - } - } - - if (!"".equals(prefix)) { - Map 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(); } } diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocator.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocator.java index 25f58672..f76a4c61 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocator.java +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocator.java @@ -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); } } diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigUtils.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigUtils.java index 1e3ad88e..ac702c91 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigUtils.java +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigUtils.java @@ -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 dataFromSecret(Secret secret, String namespace) { + LOG.debug("reading secret with name : " + secret.getMetadata().getName() + " in namespace : " + namespace); + return secretData(secret.getData()); + } + + private static Map secretData(Map data) { + Map result = new HashMap<>(CollectionUtils.newHashMap(data.size())); + data.forEach((key, value) -> result.put(key, new String(Base64.getDecoder().decode(value)).trim())); + return result; + } + } diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ContextToSourceData.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ContextToSourceData.java new file mode 100644 index 00000000..baccafee --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ContextToSourceData.java @@ -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 { + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySource.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySource.java index 6b71864d..47343c1f 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySource.java +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySource.java @@ -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 STRATEGIES = new EnumMap<>( + NormalizedSourceType.class); - public Fabric8SecretsPropertySource(KubernetesClient client, String name, String namespace, - Map 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 getSourceData(KubernetesClient client, String name, String namespace, - Map labels, boolean failFast) { - Map 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 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(); } } diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceLocator.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceLocator.java index ca1addde..98d816e5 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceLocator.java +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceLocator.java @@ -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 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); } } diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledSecretContextToSourceDataProvider.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledSecretContextToSourceDataProvider.java new file mode 100644 index 00000000..f8dfd480 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledSecretContextToSourceDataProvider.java @@ -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 { + + private static final Log LOG = LogFactory.getLog(LabeledSecretContextToSourceDataProvider.class); + + private final BiFunction sourceNameMapper; + + private LabeledSecretContextToSourceDataProvider(BiFunction sourceNameFunction) { + this.sourceNameMapper = Objects.requireNonNull(sourceNameFunction); + } + + static LabeledSecretContextToSourceDataProvider of(BiFunction 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 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 labels = source.labels(); + + Map 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 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); + }; + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/NamedConfigMapContextToSourceDataProvider.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/NamedConfigMapContextToSourceDataProvider.java new file mode 100644 index 00000000..5ee59a23 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/NamedConfigMapContextToSourceDataProvider.java @@ -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 { + + private static final Log LOG = LogFactory.getLog(NamedConfigMapContextToSourceDataProvider.class); + + private final BiFunction, Environment, Map> entriesProcessor; + + private final BiFunction sourceNameMapper; + + private final Function withPrefix; + + private NamedConfigMapContextToSourceDataProvider( + BiFunction, Environment, Map> entriesProcessor, + BiFunction sourceNameMapper, + Function withPrefix) { + this.entriesProcessor = Objects.requireNonNull(entriesProcessor); + this.sourceNameMapper = Objects.requireNonNull(sourceNameMapper); + this.withPrefix = Objects.requireNonNull(withPrefix); + } + + static NamedConfigMapContextToSourceDataProvider of( + BiFunction, Environment, Map> entriesProcessor, + BiFunction sourceNameMapper, + Function 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 propertySourceNames = new LinkedHashSet<>(); + propertySourceNames.add(initialConfigMapName); + + Map result = new HashMap<>(); + + LOG.info("Loading ConfigMap with name '" + initialConfigMapName + "' in namespace '" + namespace + "'"); + try { + Map 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 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 appName(Environment environment, NormalizedSource normalizedSource) { + return () -> getApplicationName(environment, normalizedSource.name().orElse(null), normalizedSource.target()); + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/NamedSecretContextToSourceDataProvider.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/NamedSecretContextToSourceDataProvider.java new file mode 100644 index 00000000..06149f47 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/NamedSecretContextToSourceDataProvider.java @@ -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 { + + private static final Log LOG = LogFactory.getLog(LabeledSecretContextToSourceDataProvider.class); + + private final BiFunction sourceNameMapper; + + private NamedSecretContextToSourceDataProvider(BiFunction sourceNameFunction) { + this.sourceNameMapper = Objects.requireNonNull(sourceNameFunction); + } + + static NamedSecretContextToSourceDataProvider of(BiFunction sourceNameFunction) { + return new NamedSecretContextToSourceDataProvider(sourceNameFunction); + } + + @Override + public Fabric8ContextToSourceData get() { + return context -> { + + NamedSecretNormalizedSource source = (NamedSecretNormalizedSource) context.normalizedSource(); + + Map 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); + }; + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/ConfigMapsTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/ConfigMapsTest.java index 5da4407a..d6934856 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/ConfigMapsTest.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/ConfigMapsTest.java @@ -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"); diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/EventBasedConfigurationChangeDetectorTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/EventBasedConfigurationChangeDetectorTests.java similarity index 84% rename from spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/EventBasedConfigurationChangeDetectorTests.java rename to spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/EventBasedConfigurationChangeDetectorTests.java index 3216ccb2..756e998f 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload/EventBasedConfigurationChangeDetectorTests.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/EventBasedConfigurationChangeDetectorTests.java @@ -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); diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocatorTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocatorTests.java index 334d1706..b359c9c5 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocatorTests.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocatorTests.java @@ -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); + } + } diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceTests.java index 2da67dae..b9299a7d 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceTests.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceTests.java @@ -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(); } } diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceLocatorTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceLocatorTests.java index 63eb8ee5..c5dfde9a 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceLocatorTests.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceLocatorTests.java @@ -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 diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceMockTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceMockTests.java index b6dc1458..da916d1a 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceMockTests.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceMockTests.java @@ -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 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 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)); } } diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledSecretContextToSourceDataProviderTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledSecretContextToSourceDataProviderTests.java new file mode 100644 index 00000000..66276295 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledSecretContextToSourceDataProviderTests.java @@ -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 LABELS = new LinkedHashMap<>(); + + private static final Map RED_LABEL = Map.of("color", "red"); + + private static final Map PINK_LABEL = Map.of("color", "pink"); + + private static final Map 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); + } + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/NamedConfigMapContextToSourceDataProviderTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/NamedConfigMapContextToSourceDataProviderTests.java new file mode 100644 index 00000000..3afaa255 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/NamedConfigMapContextToSourceDataProviderTests.java @@ -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 processEntries(Map map, Environment environment) { + return processAllEntries(map, environment); + } + + private static SourceData prefix(ConfigMapPrefixContext context) { + return withPrefix(context); + } + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/NamedSecretContextToSourceDataProviderTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/NamedSecretContextToSourceDataProviderTests.java new file mode 100644 index 00000000..9ea37d3a --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/NamedSecretContextToSourceDataProviderTests.java @@ -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); + } + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/locator_retry/ConfigFailFastEnabledButRetryDisabled.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/locator_retry/ConfigFailFastEnabledButRetryDisabled.java index 2b90b0ba..7cf2b960 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/locator_retry/ConfigFailFastEnabledButRetryDisabled.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/locator_retry/ConfigFailFastEnabledButRetryDisabled.java @@ -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 { diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/retry/SecretsFailFastEnabledButRetryDisabled.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/retry/SecretsFailFastEnabledButRetryDisabled.java index 9f2bf4fa..2d00828d 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/retry/SecretsFailFastEnabledButRetryDisabled.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/retry/SecretsFailFastEnabledButRetryDisabled.java @@ -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()); diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/retry/SecretsRetryDisabledButConfigRetryEnabled.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/retry/SecretsRetryDisabledButConfigRetryEnabled.java index 75e7492a..3b084716 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/retry/SecretsRetryDisabledButConfigRetryEnabled.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/retry/SecretsRetryDisabledButConfigRetryEnabled.java @@ -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()); diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/retry/SecretsRetryEnabled.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/retry/SecretsRetryEnabled.java index c7a4dff4..75ee58de 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/retry/SecretsRetryEnabled.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/retry/SecretsRetryEnabled.java @@ -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()); diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index e3eec7eb..9dd33353 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -15,4 +15,6 @@ + +