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 index 0af0002412..74ca27d417 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2013-2022 the original author or authors. + * Copyright 2013-2024 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. @@ -27,10 +27,5 @@ * @author wind57 */ public record KubernetesClientConfigContext(CoreV1Api client, NormalizedSource normalizedSource, String namespace, - Environment environment, boolean includeDefaultProfileData) { - - public KubernetesClientConfigContext(CoreV1Api client, NormalizedSource normalizedSource, String namespace, - Environment environment) { - this(client, normalizedSource, namespace, environment, true); - } + Environment environment, boolean includeDefaultProfileData, boolean namespacedBatchRead) { } diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigDataLocationResolver.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigDataLocationResolver.java index 19d7f14ff9..3dbf5f70eb 100644 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigDataLocationResolver.java +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigDataLocationResolver.java @@ -64,7 +64,8 @@ protected void registerBeans(ConfigDataLocationResolverContext resolverContext, coreV1Api, configMapProperties, namespaceProvider); if (isRetryEnabledForConfigMap(configMapProperties)) { configMapPropertySourceLocator = new ConfigDataRetryableConfigMapPropertySourceLocator( - configMapPropertySourceLocator, configMapProperties, new KubernetesClientConfigMapsCache()); + configMapPropertySourceLocator, configMapProperties, + new KubernetesClientSourcesNamespaceBatched()); } registerSingle(bootstrapContext, ConfigMapPropertySourceLocator.class, configMapPropertySourceLocator, @@ -76,7 +77,7 @@ protected void registerBeans(ConfigDataLocationResolverContext resolverContext, coreV1Api, namespaceProvider, secretsProperties); if (isRetryEnabledForSecrets(secretsProperties)) { secretsPropertySourceLocator = new ConfigDataRetryableSecretsPropertySourceLocator( - secretsPropertySourceLocator, secretsProperties, new KubernetesClientSecretsCache()); + secretsPropertySourceLocator, secretsProperties, new KubernetesClientSourcesNamespaceBatched()); } registerSingle(bootstrapContext, SecretsPropertySourceLocator.class, secretsPropertySourceLocator, 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 56a17e6a7e..9015db29a6 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 @@ -41,19 +41,20 @@ public class KubernetesClientConfigMapPropertySourceLocator extends ConfigMapPro public KubernetesClientConfigMapPropertySourceLocator(CoreV1Api coreV1Api, ConfigMapConfigProperties properties, KubernetesNamespaceProvider kubernetesNamespaceProvider) { - super(properties, new KubernetesClientConfigMapsCache()); + super(properties, new KubernetesClientSourcesNamespaceBatched()); this.coreV1Api = coreV1Api; this.kubernetesNamespaceProvider = kubernetesNamespaceProvider; } @Override - protected MapPropertySource getMapPropertySource(NormalizedSource source, ConfigurableEnvironment environment) { + protected MapPropertySource getMapPropertySource(NormalizedSource source, ConfigurableEnvironment environment, + boolean namespacedBatchRead) { String normalizedNamespace = source.namespace().orElse(null); String namespace = getApplicationNamespace(normalizedNamespace, source.target(), kubernetesNamespaceProvider); KubernetesClientConfigContext context = new KubernetesClientConfigContext(coreV1Api, source, namespace, - environment); + environment, true, namespacedBatchRead); 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 f4be4ef88e..09b99a2f0a 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 @@ -33,6 +33,10 @@ import org.springframework.core.env.Environment; import static org.springframework.cloud.kubernetes.client.KubernetesClientUtils.getApplicationNamespace; +import static org.springframework.cloud.kubernetes.client.config.KubernetesClientSourcesNamespaceBatched.strippedConfigMapsBatchRead; +import static org.springframework.cloud.kubernetes.client.config.KubernetesClientSourcesNamespaceBatched.strippedSecretsBatchRead; +import static org.springframework.cloud.kubernetes.client.config.KubernetesClientSourcesNonNamespaceBatched.strippedConfigMapsNonBatchRead; +import static org.springframework.cloud.kubernetes.client.config.KubernetesClientSourcesNonNamespaceBatched.strippedSecretsNonBatchRead; /** * @author Ryan Baxter @@ -61,42 +65,31 @@ public static Set namespaces(KubernetesNamespaceProvider provider, Confi return namespaces; } - /** - *
-	 *     1. read all secrets in the provided namespace
-	 *     2. from the above, filter the ones that we care about (filter by labels)
-	 *     3. with secret names from (2), find out if there are any profile based secrets (if profiles is not empty)
-	 *     4. concat (2) and (3) and these are the secrets we are interested in
-	 *     5. see if any of the secrets from (4) has a single yaml/properties file
-	 *     6. gather all the names of the secrets (from 4) + data they hold
-	 * 
- */ - static MultipleSourcesContainer secretsDataByLabels(CoreV1Api coreV1Api, String namespace, - Map labels, Environment environment, Set profiles) { - List strippedSecrets = strippedSecrets(coreV1Api, namespace); - if (strippedSecrets.isEmpty()) { - return MultipleSourcesContainer.empty(); - } - return ConfigUtils.processLabeledData(strippedSecrets, environment, labels, namespace, profiles, DECODE); - } - /** *
 	 *     1. read all config maps in the provided namespace
-	 *     2. from the above, filter the ones that we care about (filter by labels)
-	 *     3. with config maps names from (2), find out if there are any profile based ones (if profiles is not empty)
-	 *     4. concat (2) and (3) and these are the config maps we are interested in
-	 *     5. see if any from (4) has a single yaml/properties file
-	 *     6. gather all the names of the config maps (from 4) + data they hold
+	 *     2. from the above, filter the ones that we care about (by name)
+	 *     3. see if any of the config maps has a single yaml/properties file
+	 *     4. gather all the names of the config maps + data they hold
 	 * 
*/ - static MultipleSourcesContainer configMapsDataByLabels(CoreV1Api coreV1Api, String namespace, - Map labels, Environment environment, Set profiles) { - List strippedConfigMaps = strippedConfigMaps(coreV1Api, namespace); - if (strippedConfigMaps.isEmpty()) { - return MultipleSourcesContainer.empty(); + static MultipleSourcesContainer configMapsDataByName(CoreV1Api client, String namespace, + LinkedHashSet sourceNames, Environment environment, boolean includeDefaultProfileData, + boolean namespacedBatchRead) { + + List strippedConfigMaps; + + if (namespacedBatchRead) { + LOG.debug("Will read all configmaps in namespace : " + namespace); + strippedConfigMaps = strippedConfigMapsBatchRead(client, namespace); + } + else { + LOG.debug("Will read individual configmaps in namespace : " + namespace + " with names : " + sourceNames); + strippedConfigMaps = strippedConfigMapsNonBatchRead(client, namespace, sourceNames); } - return ConfigUtils.processLabeledData(strippedConfigMaps, environment, labels, namespace, profiles, DECODE); + + return ConfigUtils.processNamedData(strippedConfigMaps, environment, sourceNames, namespace, false, + includeDefaultProfileData); } /** @@ -107,49 +100,73 @@ static MultipleSourcesContainer configMapsDataByLabels(CoreV1Api coreV1Api, Stri * 4. gather all the names of the secrets + decoded data they hold * */ - static MultipleSourcesContainer secretsDataByName(CoreV1Api coreV1Api, String namespace, - LinkedHashSet sourceNames, Environment environment, boolean includeDefaultProfileData) { - List strippedSecrets = strippedSecrets(coreV1Api, namespace); - if (strippedSecrets.isEmpty()) { - return MultipleSourcesContainer.empty(); + static MultipleSourcesContainer secretsDataByName(CoreV1Api client, String namespace, + LinkedHashSet sourceNames, Environment environment, boolean includeDefaultProfileData, + boolean namespacedBatchRead) { + + List strippedSecrets; + + if (namespacedBatchRead) { + LOG.debug("Will read all secrets in namespace : " + namespace); + strippedSecrets = strippedSecretsBatchRead(client, namespace); + } + else { + LOG.debug("Will read individual secrets in namespace : " + namespace + " with names : " + sourceNames); + strippedSecrets = strippedSecretsNonBatchRead(client, namespace, sourceNames); } - return ConfigUtils.processNamedData(strippedSecrets, environment, sourceNames, namespace, DECODE, + + return ConfigUtils.processNamedData(strippedSecrets, environment, sourceNames, namespace, false, includeDefaultProfileData); } /** *
 	 *     1. read all config maps in the provided namespace
-	 *     2. from the above, filter the ones that we care about (by name)
-	 *     3. see if any of the config maps has a single yaml/properties file
+	 *     2. from the above, filter the ones that we care about (filter by labels)
+	 *     3. see if any from (2) has a single yaml/properties file
 	 *     4. gather all the names of the config maps + data they hold
 	 * 
*/ - static MultipleSourcesContainer configMapsDataByName(CoreV1Api coreV1Api, String namespace, - LinkedHashSet sourceNames, Environment environment, boolean includeDefaultProfileData) { - List strippedConfigMaps = strippedConfigMaps(coreV1Api, namespace); - if (strippedConfigMaps.isEmpty()) { - return MultipleSourcesContainer.empty(); - } - return ConfigUtils.processNamedData(strippedConfigMaps, environment, sourceNames, namespace, DECODE, - includeDefaultProfileData); - } + static MultipleSourcesContainer configMapsDataByLabels(CoreV1Api client, String namespace, + Map labels, Environment environment, boolean namespacedBatchRead) { + + List strippedConfigMaps; - private static List strippedConfigMaps(CoreV1Api coreV1Api, String namespace) { - List strippedConfigMaps = KubernetesClientConfigMapsCache.byNamespace(coreV1Api, - namespace); - if (strippedConfigMaps.isEmpty()) { - LOG.debug("No configmaps in namespace '" + namespace + "'"); + if (namespacedBatchRead) { + LOG.debug("Will read all configmaps in namespace : " + namespace); + strippedConfigMaps = strippedConfigMapsBatchRead(client, namespace); } - return strippedConfigMaps; + else { + LOG.debug("Will read individual configmaps in namespace : " + namespace + " with labels : " + labels); + strippedConfigMaps = strippedConfigMapsNonBatchRead(client, namespace, labels); + } + + return ConfigUtils.processLabeledData(strippedConfigMaps, environment, labels, namespace, false); } - private static List strippedSecrets(CoreV1Api coreV1Api, String namespace) { - List strippedSecrets = KubernetesClientSecretsCache.byNamespace(coreV1Api, namespace); - if (strippedSecrets.isEmpty()) { - LOG.debug("No secrets in namespace '" + namespace + "'"); + /** + *
+	 *     1. read all secrets in the provided namespace
+	 *     2. from the above, filter the ones that we care about (filter by labels)
+	 *     3. see if any of the secrets from (2) has a single yaml/properties file
+	 *     4. gather all the names of the secrets + data they hold
+	 * 
+ */ + static MultipleSourcesContainer secretsDataByLabels(CoreV1Api client, String namespace, Map labels, + Environment environment, boolean namespacedBatchRead) { + + List strippedSecrets; + + if (namespacedBatchRead) { + LOG.debug("Will read all secrets in namespace : " + namespace); + strippedSecrets = strippedSecretsBatchRead(client, namespace); } - return strippedSecrets; + else { + LOG.debug("Will read individual secrets in namespace : " + namespace + " with labels : " + labels); + strippedSecrets = strippedSecretsNonBatchRead(client, namespace, labels); + } + + return ConfigUtils.processLabeledData(strippedSecrets, environment, labels, namespace, false); } } diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsCache.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsCache.java deleted file mode 100644 index 49df6f8306..0000000000 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsCache.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.apis.CoreV1Api; -import io.kubernetes.client.openapi.models.V1Secret; -import org.apache.commons.logging.LogFactory; - -import org.springframework.cloud.kubernetes.commons.config.SecretsCache; -import org.springframework.cloud.kubernetes.commons.config.StrippedSourceContainer; -import org.springframework.core.log.LogAccessor; -import org.springframework.util.ObjectUtils; - -/** - * A cache of V1ConfigMap(s) per namespace. Makes sure we read config maps only once from - * a namespace. - * - * @author wind57 - */ -public class KubernetesClientSecretsCache implements SecretsCache { - - private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(KubernetesClientConfigMapsCache.class)); - - /** - * at the moment our loading of config maps is using a single thread, but might change - * in the future, thus a thread safe structure. - */ - private static final ConcurrentHashMap> CACHE = new ConcurrentHashMap<>(); - - @Override - public void discardAll() { - CACHE.clear(); - } - - static List byNamespace(CoreV1Api coreV1Api, String namespace) { - boolean[] b = new boolean[1]; - List result = CACHE.computeIfAbsent(namespace, x -> { - try { - b[0] = true; - return strippedSecrets(coreV1Api - .listNamespacedSecret(namespace, null, null, null, null, null, null, null, null, null, null, null) - .getItems()); - } - catch (ApiException apiException) { - throw new RuntimeException(apiException.getResponseBody(), apiException); - } - }); - - if (b[0]) { - LOG.debug(() -> "Loaded all secrets in namespace '" + namespace + "'"); - } - else { - LOG.debug(() -> "Loaded (from cache) all secrets in namespace '" + namespace + "'"); - } - - return result; - } - - private static List strippedSecrets(List secrets) { - return secrets.stream() - .map(secret -> new StrippedSourceContainer(secret.getMetadata().getLabels(), secret.getMetadata().getName(), - transform(secret.getData()))) - .toList(); - } - - private static Map transform(Map in) { - return ObjectUtils.isEmpty(in) ? Map.of() - : in.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, en -> new String(en.getValue()))); - } - -} 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 057a43d90f..d9440b4258 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 @@ -41,19 +41,20 @@ public class KubernetesClientSecretsPropertySourceLocator extends SecretsPropert public KubernetesClientSecretsPropertySourceLocator(CoreV1Api coreV1Api, KubernetesNamespaceProvider kubernetesNamespaceProvider, SecretsConfigProperties secretsConfigProperties) { - super(secretsConfigProperties, new KubernetesClientSecretsCache()); + super(secretsConfigProperties, new KubernetesClientSourcesNamespaceBatched()); this.coreV1Api = coreV1Api; this.kubernetesNamespaceProvider = kubernetesNamespaceProvider; } @Override - protected SecretsPropertySource getPropertySource(ConfigurableEnvironment environment, NormalizedSource source) { + protected SecretsPropertySource getPropertySource(ConfigurableEnvironment environment, NormalizedSource source, + boolean namespacedBatchRead) { String normalizedNamespace = source.namespace().orElse(null); String namespace = getApplicationNamespace(normalizedNamespace, source.target(), kubernetesNamespaceProvider); KubernetesClientConfigContext context = new KubernetesClientConfigContext(coreV1Api, source, namespace, - environment); + environment, true, namespacedBatchRead); return new KubernetesClientSecretsPropertySource(context); } diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapsCache.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSourcesNamespaceBatched.java similarity index 51% rename from spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapsCache.java rename to spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSourcesNamespaceBatched.java index 3a0d046db6..6fb8d35dd2 100644 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapsCache.java +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSourcesNamespaceBatched.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2022 the original author or authors. + * Copyright 2013-2024 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. @@ -21,40 +21,45 @@ import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.apis.CoreV1Api; -import io.kubernetes.client.openapi.models.V1ConfigMap; import org.apache.commons.logging.LogFactory; import org.springframework.cloud.kubernetes.commons.config.ConfigMapCache; +import org.springframework.cloud.kubernetes.commons.config.SecretsCache; import org.springframework.cloud.kubernetes.commons.config.StrippedSourceContainer; import org.springframework.core.log.LogAccessor; /** - * A cache of V1ConfigMap(s) per namespace. Makes sure we read config maps only once from - * a namespace. - * * @author wind57 */ -public final class KubernetesClientConfigMapsCache implements ConfigMapCache { +public final class KubernetesClientSourcesNamespaceBatched implements SecretsCache, ConfigMapCache { - private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(KubernetesClientConfigMapsCache.class)); + private static final LogAccessor LOG = new LogAccessor( + LogFactory.getLog(KubernetesClientSourcesNamespaceBatched.class)); /** * at the moment our loading of config maps is using a single thread, but might change * in the future, thus a thread safe structure. */ - private static final ConcurrentHashMap> CACHE = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap> SECRETS_CACHE = new ConcurrentHashMap<>(); + + private static final ConcurrentHashMap> CONFIG_MAPS_CACHE = new ConcurrentHashMap<>(); + + @Override + public void discardSecrets() { + SECRETS_CACHE.clear(); + } @Override - public void discardAll() { - CACHE.clear(); + public void discardConfigMaps() { + CONFIG_MAPS_CACHE.clear(); } - static List byNamespace(CoreV1Api coreV1Api, String namespace) { + static List strippedConfigMapsBatchRead(CoreV1Api coreV1Api, String namespace) { boolean[] b = new boolean[1]; - List result = CACHE.computeIfAbsent(namespace, x -> { + List result = CONFIG_MAPS_CACHE.computeIfAbsent(namespace, x -> { try { b[0] = true; - return strippedConfigMaps(coreV1Api + return KubernetesClientSourcesStripper.strippedConfigMaps(coreV1Api .listNamespacedConfigMap(namespace, null, null, null, null, null, null, null, null, null, null, null) .getItems()); @@ -74,11 +79,28 @@ static List byNamespace(CoreV1Api coreV1Api, String nam return result; } - private static List strippedConfigMaps(List configMaps) { - return configMaps.stream() - .map(configMap -> new StrippedSourceContainer(configMap.getMetadata().getLabels(), - configMap.getMetadata().getName(), configMap.getData())) - .toList(); + static List strippedSecretsBatchRead(CoreV1Api coreV1Api, String namespace) { + boolean[] b = new boolean[1]; + List result = SECRETS_CACHE.computeIfAbsent(namespace, x -> { + try { + b[0] = true; + return KubernetesClientSourcesStripper.strippedSecrets(coreV1Api + .listNamespacedSecret(namespace, null, null, null, null, null, null, null, null, null, null, null) + .getItems()); + } + catch (ApiException apiException) { + throw new RuntimeException(apiException.getResponseBody(), apiException); + } + }); + + if (b[0]) { + LOG.debug(() -> "Loaded all secrets in namespace '" + namespace + "'"); + } + else { + LOG.debug(() -> "Loaded (from cache) all secrets in namespace '" + namespace + "'"); + } + + return result; } } diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSourcesNonNamespaceBatched.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSourcesNonNamespaceBatched.java new file mode 100644 index 0000000000..2665315441 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSourcesNonNamespaceBatched.java @@ -0,0 +1,167 @@ +/* + * Copyright 2013-2024 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.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1ConfigMap; +import io.kubernetes.client.openapi.models.V1Secret; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.kubernetes.commons.config.StrippedSourceContainer; +import org.springframework.core.log.LogAccessor; + +final class KubernetesClientSourcesNonNamespaceBatched { + + private static final LogAccessor LOG = new LogAccessor( + LogFactory.getLog(KubernetesClientSourcesNonNamespaceBatched.class)); + + private KubernetesClientSourcesNonNamespaceBatched() { + + } + + /** + * read configmaps by name, one by one, without caching them. + */ + static List strippedConfigMapsNonBatchRead(CoreV1Api client, String namespace, + LinkedHashSet sourceNames) { + + List configMaps = new ArrayList<>(sourceNames.size()); + + for (String sourceName : sourceNames) { + V1ConfigMap configMap = null; + try { + configMap = client.readNamespacedConfigMap(sourceName, namespace, null); + } + catch (ApiException e) { + KubernetesClientSourcesStripper.handleApiException(e, sourceName); + } + if (configMap != null) { + LOG.debug("Loaded config map '" + sourceName + "'"); + configMaps.add(configMap); + } + } + + List strippedConfigMaps = KubernetesClientSourcesStripper + .strippedConfigMaps(configMaps); + + if (strippedConfigMaps.isEmpty()) { + LOG.debug("No configmaps in namespace '" + namespace + "'"); + } + + return strippedConfigMaps; + } + + /** + * read secrets by name, one by one, without caching them. + */ + static List strippedSecretsNonBatchRead(CoreV1Api client, String namespace, + LinkedHashSet sourceNames) { + + List secrets = new ArrayList<>(sourceNames.size()); + + for (String sourceName : sourceNames) { + V1Secret secret = null; + try { + secret = client.readNamespacedSecret(sourceName, namespace, null); + } + catch (ApiException e) { + KubernetesClientSourcesStripper.handleApiException(e, sourceName); + } + if (secret != null) { + LOG.debug("Loaded config map '" + sourceName + "'"); + secrets.add(secret); + } + } + + List strippedSecrets = KubernetesClientSourcesStripper.strippedSecrets(secrets); + + if (strippedSecrets.isEmpty()) { + LOG.debug("No secrets in namespace '" + namespace + "'"); + } + + return strippedSecrets; + } + + /** + * read configmaps by labels, without caching them. + */ + static List strippedConfigMapsNonBatchRead(CoreV1Api client, String namespace, + Map labels) { + + List configMaps; + try { + configMaps = client + .listNamespacedConfigMap(namespace, null, null, null, null, labelSelector(labels), null, null, null, + null, null, null) + .getItems(); + } + catch (ApiException e) { + throw new RuntimeException(e.getResponseBody(), e); + } + for (V1ConfigMap configMap : configMaps) { + LOG.debug("Loaded config map '" + configMap.getMetadata().getName() + "'"); + } + + List strippedConfigMaps = KubernetesClientSourcesStripper + .strippedConfigMaps(configMaps); + if (strippedConfigMaps.isEmpty()) { + LOG.debug("No configmaps in namespace '" + namespace + "'"); + } + + return strippedConfigMaps; + } + + /** + * read secrets by labels, without caching them. + */ + static List strippedSecretsNonBatchRead(CoreV1Api client, String namespace, + Map labels) { + + List secrets; + try { + secrets = client + .listNamespacedSecret(namespace, null, null, null, null, labelSelector(labels), null, null, null, null, + null, null) + .getItems(); + } + catch (ApiException e) { + throw new RuntimeException(e.getResponseBody(), e); + } + for (V1Secret secret : secrets) { + LOG.debug("Loaded secret '" + secret.getMetadata().getName() + "'"); + } + + List strippedSecrets = KubernetesClientSourcesStripper.strippedSecrets(secrets); + if (strippedSecrets.isEmpty()) { + LOG.debug("No secrets in namespace '" + namespace + "'"); + } + + return strippedSecrets; + } + + private static String labelSelector(Map labels) { + return labels.entrySet().stream().map(en -> en.getKey() + "=" + en.getValue()).collect(Collectors.joining("&")); + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSourcesStripper.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSourcesStripper.java new file mode 100644 index 0000000000..8ac6e78da5 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSourcesStripper.java @@ -0,0 +1,67 @@ +/* + * Copyright 2013-2024 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.List; +import java.util.Map; +import java.util.stream.Collectors; + +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.models.V1ConfigMap; +import io.kubernetes.client.openapi.models.V1Secret; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.kubernetes.commons.config.StrippedSourceContainer; +import org.springframework.core.log.LogAccessor; +import org.springframework.util.ObjectUtils; + +/** + * @author wind57 + */ +interface KubernetesClientSourcesStripper { + + LogAccessor LOG = new LogAccessor(LogFactory.getLog(KubernetesClientSourcesStripper.class)); + + static List strippedSecrets(List secrets) { + return secrets.stream() + .map(secret -> new StrippedSourceContainer(secret.getMetadata().getLabels(), secret.getMetadata().getName(), + transform(secret.getData()))) + .toList(); + } + + static List strippedConfigMaps(List configMaps) { + return configMaps.stream() + .map(configMap -> new StrippedSourceContainer(configMap.getMetadata().getLabels(), + configMap.getMetadata().getName(), configMap.getData())) + .toList(); + } + + static void handleApiException(ApiException e, String sourceName) { + if (e.getCode() == 404) { + LOG.warn("source with name : " + sourceName + " not found. Ignoring"); + } + else { + throw new RuntimeException(e.getResponseBody(), e); + } + } + + private static Map transform(Map in) { + return ObjectUtils.isEmpty(in) ? Map.of() + : in.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, en -> new String(en.getValue()))); + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/LabeledConfigMapContextToSourceDataProvider.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/LabeledConfigMapContextToSourceDataProvider.java index 054fb767f1..aaf1d9f5cb 100644 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/LabeledConfigMapContextToSourceDataProvider.java +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/LabeledConfigMapContextToSourceDataProvider.java @@ -17,7 +17,6 @@ package org.springframework.cloud.kubernetes.client.config; import java.util.Map; -import java.util.Set; import java.util.function.Supplier; import org.springframework.cloud.kubernetes.commons.config.LabeledConfigMapNormalizedSource; @@ -48,13 +47,12 @@ public KubernetesClientContextToSourceData get() { return new LabeledSourceData() { @Override - public MultipleSourcesContainer dataSupplier(Map labels, Set profiles) { + public MultipleSourcesContainer dataSupplier(Map labels) { return KubernetesClientConfigUtils.configMapsDataByLabels(context.client(), context.namespace(), - labels, context.environment(), profiles); + labels, context.environment(), context.namespacedBatchRead()); } - }.compute(source.labels(), source.prefix(), source.target(), source.profileSpecificSources(), - source.failFast(), context.namespace(), context.environment().getActiveProfiles()); + }.compute(source.labels(), source.prefix(), source.target(), source.failFast(), context.namespace()); }; } 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 index 9cf72cbc0b..a20905aefb 100644 --- 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 @@ -17,7 +17,6 @@ package org.springframework.cloud.kubernetes.client.config; import java.util.Map; -import java.util.Set; import java.util.function.Supplier; import org.springframework.cloud.kubernetes.commons.config.LabeledSecretNormalizedSource; @@ -55,13 +54,12 @@ public KubernetesClientContextToSourceData get() { return new LabeledSourceData() { @Override - public MultipleSourcesContainer dataSupplier(Map labels, Set profiles) { + public MultipleSourcesContainer dataSupplier(Map labels) { return KubernetesClientConfigUtils.secretsDataByLabels(context.client(), context.namespace(), - labels, context.environment(), profiles); + labels, context.environment(), context.namespacedBatchRead()); } - }.compute(source.labels(), source.prefix(), source.target(), source.profileSpecificSources(), - source.failFast(), context.namespace(), context.environment().getActiveProfiles()); + }.compute(source.labels(), source.prefix(), source.target(), source.failFast(), context.namespace()); }; } 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 index 56645a4c51..5709791937 100644 --- 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 @@ -55,7 +55,8 @@ protected String generateSourceName(String target, String sourceName, String nam @Override public MultipleSourcesContainer dataSupplier(LinkedHashSet sourceNames) { return KubernetesClientConfigUtils.configMapsDataByName(context.client(), context.namespace(), - sourceNames, context.environment(), context.includeDefaultProfileData()); + sourceNames, context.environment(), context.includeDefaultProfileData(), + context.namespacedBatchRead()); } }.compute(source.name().orElseThrow(), source.prefix(), source.target(), source.profileSpecificSources(), source.failFast(), context.namespace(), context.environment().getActiveProfiles()); 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 index bfde140060..c77c42cc21 100644 --- 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 @@ -54,7 +54,8 @@ protected String generateSourceName(String target, String sourceName, String nam @Override public MultipleSourcesContainer dataSupplier(LinkedHashSet sourceNames) { return KubernetesClientConfigUtils.secretsDataByName(context.client(), context.namespace(), - sourceNames, context.environment(), context.includeDefaultProfileData()); + sourceNames, context.environment(), context.includeDefaultProfileData(), + context.namespacedBatchRead()); } }.compute(source.name().orElseThrow(), source.prefix(), source.target(), source.profileSpecificSources(), source.failFast(), context.namespace(), context.environment().getActiveProfiles()); diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapErrorOnReadingSourceTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapErrorOnReadingSourceTests.java index 13174e7cc3..6c7ef4a4f2 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapErrorOnReadingSourceTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapErrorOnReadingSourceTests.java @@ -56,6 +56,8 @@ @ExtendWith(OutputCaptureExtension.class) class KubernetesClientConfigMapErrorOnReadingSourceTests { + private static final boolean NAMESPACE_BATCHED = true; + private static final V1ConfigMapList SINGLE_CONFIGMAP_LIST = new V1ConfigMapList() .addItemsItem(new V1ConfigMapBuilder() .withMetadata( @@ -110,7 +112,7 @@ void namedSingleConfigMapFails(CapturedOutput output) { stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(), - Map.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT); + Map.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT, NAMESPACE_BATCHED); CoreV1Api api = new CoreV1Api(); KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api, @@ -153,7 +155,7 @@ void namedTwoConfigMapsOneFails(CapturedOutput output) { ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(sourceOne, sourceTwo), Map.of(), true, null, namespace, false, true, false, - RetryProperties.DEFAULT); + RetryProperties.DEFAULT, NAMESPACE_BATCHED); CoreV1Api api = new CoreV1Api(); KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api, @@ -200,7 +202,7 @@ void namedTwoConfigMapsBothFail(CapturedOutput output) { ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(sourceOne, sourceTwo), Map.of(), true, null, namespace, false, true, false, - RetryProperties.DEFAULT); + RetryProperties.DEFAULT, NAMESPACE_BATCHED); CoreV1Api api = new CoreV1Api(); KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api, @@ -243,7 +245,8 @@ void labeledSingleConfigMapFails(CapturedOutput output) { null, null, null); ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), - List.of(configMapSource), labels, true, null, namespace, false, true, false, RetryProperties.DEFAULT); + List.of(configMapSource), labels, true, null, namespace, false, true, false, RetryProperties.DEFAULT, + NAMESPACE_BATCHED); CoreV1Api api = new CoreV1Api(); KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api, @@ -295,7 +298,7 @@ void labeledTwoConfigMapsOneFails(CapturedOutput output) { ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(sourceOne, sourceTwo), Map.of("one", "1", "two", "2"), true, null, namespace, false, true, - false, RetryProperties.DEFAULT); + false, RetryProperties.DEFAULT, NAMESPACE_BATCHED); CoreV1Api api = new CoreV1Api(); KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api, @@ -349,7 +352,7 @@ void labeledTwoConfigMapsBothFail(CapturedOutput output) { ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(sourceOne, sourceTwo), Map.of("one", "1", "two", "2"), true, null, namespace, false, true, - false, RetryProperties.DEFAULT); + false, RetryProperties.DEFAULT, NAMESPACE_BATCHED); CoreV1Api api = new CoreV1Api(); KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api, 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 04bbefb870..7ba4aac25c 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 @@ -63,6 +63,8 @@ @ExtendWith(OutputCaptureExtension.class) class KubernetesClientConfigMapPropertySourceLocatorTests { + private static final boolean NAMESPACED_BATCH_READ = true; + private static final V1ConfigMapList PROPERTIES_CONFIGMAP_LIST = new V1ConfigMapList() .addItemsItem(new V1ConfigMapBuilder() .withMetadata(new V1ObjectMetaBuilder().withName("bootstrap-640") @@ -106,7 +108,8 @@ void locateWithoutSources() { stubFor(get("/api/v1/namespaces/default/configmaps") .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(PROPERTIES_CONFIGMAP_LIST)))); ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(), - Map.of(), true, "bootstrap-640", null, false, false, false, RetryProperties.DEFAULT); + Map.of(), true, "bootstrap-640", null, false, false, false, RetryProperties.DEFAULT, + NAMESPACED_BATCH_READ); MockEnvironment mockEnvironment = new MockEnvironment(); mockEnvironment.setProperty("spring.cloud.kubernetes.client.namespace", "default"); PropertySource propertySource = new KubernetesClientConfigMapPropertySourceLocator(api, @@ -125,7 +128,8 @@ void locateWithSources() { ConfigMapConfigProperties.Source source = new ConfigMapConfigProperties.Source("bootstrap-640", "default", Collections.emptyMap(), null, null, null); ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), - List.of(source), Map.of(), true, "fake-name", null, false, false, false, RetryProperties.DEFAULT); + List.of(source), Map.of(), true, "fake-name", null, false, false, false, RetryProperties.DEFAULT, + NAMESPACED_BATCH_READ); PropertySource propertySource = new KubernetesClientConfigMapPropertySourceLocator(api, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())) @@ -148,7 +152,8 @@ void testLocateWithoutNamespaceConstructor() { .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(PROPERTIES_CONFIGMAP_LIST)))); ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(), - Map.of(), true, "bootstrap-640", null, false, false, false, RetryProperties.DEFAULT); + Map.of(), true, "bootstrap-640", null, false, false, false, RetryProperties.DEFAULT, + NAMESPACED_BATCH_READ); assertThatThrownBy(() -> new KubernetesClientConfigMapPropertySourceLocator(api, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())) @@ -168,7 +173,8 @@ void testLocateWithoutNamespace() { stubFor(get("/api/v1/namespaces/default/configmaps") .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(PROPERTIES_CONFIGMAP_LIST)))); ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(), - Map.of(), true, "bootstrap-640", null, false, false, false, RetryProperties.DEFAULT); + Map.of(), true, "bootstrap-640", null, false, false, false, RetryProperties.DEFAULT, + NAMESPACED_BATCH_READ); assertThatThrownBy(() -> new KubernetesClientConfigMapPropertySourceLocator(api, configMapConfigProperties, new KubernetesNamespaceProvider(ENV)) .locate(ENV)).isInstanceOf(NamespaceResolutionFailedException.class); @@ -181,7 +187,8 @@ public void locateShouldThrowExceptionOnFailureWhenFailFastIsEnabled() { .willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(), - Map.of(), true, "bootstrap-640", "default", false, false, true, RetryProperties.DEFAULT); + Map.of(), true, "bootstrap-640", "default", false, false, true, RetryProperties.DEFAULT, + NAMESPACED_BATCH_READ); KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -197,7 +204,8 @@ public void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled(Capture .willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(), - Map.of(), true, "bootstrap-640", "default", false, false, false, RetryProperties.DEFAULT); + Map.of(), true, "bootstrap-640", "default", false, false, false, RetryProperties.DEFAULT, + NAMESPACED_BATCH_READ); KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); 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 51916d24c9..df63c7dbce 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 @@ -54,6 +54,8 @@ */ class KubernetesClientConfigMapPropertySourceTests { + private static final boolean NAMESPACED_BATCH_READ = true; + private static final V1ConfigMapList PROPERTIES_CONFIGMAP_LIST = new V1ConfigMapList() .addItemsItem(new V1ConfigMapBuilder() .withMetadata(new V1ObjectMetaBuilder().withName("bootstrap-640") @@ -97,7 +99,7 @@ public static void after() { @AfterEach public void afterEach() { WireMock.reset(); - new KubernetesClientConfigMapsCache().discardAll(); + new KubernetesClientSourcesNamespaceBatched().discardConfigMaps(); } @Test @@ -108,7 +110,7 @@ public void propertiesFile() { NormalizedSource source = new NamedConfigMapNormalizedSource("bootstrap-640", "default", false, true); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, "default", - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientConfigMapPropertySource propertySource = new KubernetesClientConfigMapPropertySource(context); verify(getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps"))); @@ -129,7 +131,7 @@ public void yamlFile() { NormalizedSource source = new NamedConfigMapNormalizedSource("bootstrap-641", "default", false, true); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, "default", - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientConfigMapPropertySource propertySource = new KubernetesClientConfigMapPropertySource(context); verify(getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps"))); @@ -151,7 +153,7 @@ public void propertiesFileWithPrefix() { ConfigUtils.Prefix prefix = ConfigUtils.findPrefix("prefix", false, false, null); NormalizedSource source = new NamedConfigMapNormalizedSource("bootstrap-640", "default", false, prefix, true); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, "default", - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientConfigMapPropertySource propertySource = new KubernetesClientConfigMapPropertySource(context); verify(getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps"))); @@ -171,7 +173,7 @@ void constructorWithNamespaceMustNotFail() { ConfigUtils.Prefix prefix = ConfigUtils.findPrefix("prefix", false, false, null); NormalizedSource source = new NamedConfigMapNormalizedSource("bootstrap-640", "default", false, prefix, true); KubernetesClientConfigContext context = new KubernetesClientConfigContext(new CoreV1Api(), source, "default", - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); assertThat(new KubernetesClientConfigMapPropertySource(context)).isNotNull(); } @@ -184,7 +186,7 @@ public void constructorShouldThrowExceptionOnFailureWhenFailFastIsEnabled() { ConfigUtils.Prefix prefix = ConfigUtils.findPrefix("prefix", false, false, null); NormalizedSource source = new NamedConfigMapNormalizedSource("my-config", "default", true, prefix, true); KubernetesClientConfigContext context = new KubernetesClientConfigContext(new CoreV1Api(), source, "default", - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); assertThatThrownBy(() -> new KubernetesClientConfigMapPropertySource(context)) .isInstanceOf(IllegalStateException.class) @@ -200,7 +202,7 @@ public void constructorShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() ConfigUtils.Prefix prefix = ConfigUtils.findPrefix("prefix", false, false, null); NormalizedSource source = new NamedConfigMapNormalizedSource("my-config", "default", false, prefix, true); KubernetesClientConfigContext context = new KubernetesClientConfigContext(new CoreV1Api(), source, "default", - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); 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/KubernetesClientSecretsPropertySourceLocatorTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsPropertySourceLocatorTests.java index 030c62b968..debb565275 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 @@ -58,6 +58,8 @@ @ExtendWith(OutputCaptureExtension.class) class KubernetesClientSecretsPropertySourceLocatorTests { + private static final boolean NAMESPACED_BATCH_READ = true; + private static final String LIST_API = "/api/v1/namespaces/default/secrets"; private static final String LIST_BODY = """ @@ -147,7 +149,8 @@ void getLocateWithSources() { Collections.emptyMap(), null, null, null); SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), - List.of(source1, source2), true, "app", "default", false, true, false, RetryProperties.DEFAULT); + List.of(source1, source2), true, "app", "default", false, true, false, RetryProperties.DEFAULT, + NAMESPACED_BATCH_READ); PropertySource propertySource = new KubernetesClientSecretsPropertySourceLocator(api, new KubernetesNamespaceProvider(new MockEnvironment()), secretsConfigProperties) @@ -161,7 +164,8 @@ void getLocateWithOutSources() { CoreV1Api api = new CoreV1Api(); stubFor(get(LIST_API).willReturn(aResponse().withStatus(200).withBody(LIST_BODY))); SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), - List.of(), true, "db-secret", "default", false, true, false, RetryProperties.DEFAULT); + List.of(), true, "db-secret", "default", false, true, false, RetryProperties.DEFAULT, + NAMESPACED_BATCH_READ); PropertySource propertySource = new KubernetesClientSecretsPropertySourceLocator(api, new KubernetesNamespaceProvider(new MockEnvironment()), secretsConfigProperties) @@ -183,7 +187,7 @@ void testLocateWithoutNamespaceConstructor() { stubFor(get(LIST_API).willReturn(aResponse().withStatus(200).withBody(LIST_BODY))); SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), - List.of(), true, "db-secret", "", false, true, false, RetryProperties.DEFAULT); + List.of(), true, "db-secret", "", false, true, false, RetryProperties.DEFAULT, NAMESPACED_BATCH_READ); assertThatThrownBy(() -> new KubernetesClientSecretsPropertySourceLocator(api, new KubernetesNamespaceProvider(new MockEnvironment()), secretsConfigProperties) @@ -196,7 +200,8 @@ void locateShouldThrowExceptionOnFailureWhenFailFastIsEnabled() { stubFor(get(LIST_API).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), - List.of(), true, "db-secret", "default", false, true, true, RetryProperties.DEFAULT); + List.of(), true, "db-secret", "default", false, true, true, RetryProperties.DEFAULT, + NAMESPACED_BATCH_READ); KubernetesClientSecretsPropertySourceLocator locator = new KubernetesClientSecretsPropertySourceLocator(api, new KubernetesNamespaceProvider(new MockEnvironment()), secretsConfigProperties); @@ -211,7 +216,8 @@ void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled(CapturedOutput stubFor(get(LIST_API).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), - List.of(), true, "db-secret", "default", false, true, false, RetryProperties.DEFAULT); + List.of(), true, "db-secret", "default", false, true, false, RetryProperties.DEFAULT, + NAMESPACED_BATCH_READ); KubernetesClientSecretsPropertySourceLocator locator = new KubernetesClientSecretsPropertySourceLocator(api, new KubernetesNamespaceProvider(new MockEnvironment()), secretsConfigProperties); 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 50173d3918..c8995a661d 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 @@ -57,6 +57,8 @@ */ class KubernetesClientSecretsPropertySourceTests { + private static final boolean NAMESPACED_BATCH_READ = true; + private static final String API = "/api/v1/namespaces/default/secrets"; private static final V1SecretList SECRET_LIST = new V1SecretListBuilder() @@ -152,7 +154,7 @@ static void after() { @AfterEach void afterEach() { WireMock.reset(); - new KubernetesClientSecretsCache().discardAll(); + new KubernetesClientSourcesNamespaceBatched().discardSecrets(); } @Test @@ -163,7 +165,7 @@ void emptyDataSecretTest() { NormalizedSource source = new NamedSecretNormalizedSource("db-secret", "default", false, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, "default", - new MockEnvironment()); + new MockEnvironment(), true, NAMESPACED_BATCH_READ); KubernetesClientSecretsPropertySource propertySource = new KubernetesClientSecretsPropertySource(context); assertThat(propertySource.getName()).isEqualTo("secret.db-secret.default"); @@ -177,7 +179,7 @@ void secretsTest() { NormalizedSource source = new NamedSecretNormalizedSource("db-secret", "default", false, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, "default", - new MockEnvironment()); + new MockEnvironment(), true, NAMESPACED_BATCH_READ); KubernetesClientSecretsPropertySource propertySource = new KubernetesClientSecretsPropertySource(context); assertThat(propertySource.containsProperty("password")).isTrue(); @@ -193,9 +195,9 @@ void secretLabelsTest() { Map labels = new HashMap<>(); labels.put("spring.cloud.kubernetes.secret", "true"); - NormalizedSource source = new LabeledSecretNormalizedSource("default", labels, false, false); + NormalizedSource source = new LabeledSecretNormalizedSource("default", labels, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, "default", - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientSecretsPropertySource propertySource = new KubernetesClientSecretsPropertySource(context); assertThat(propertySource.containsProperty("spring.rabbitmq.password")).isTrue(); @@ -209,7 +211,7 @@ void constructorShouldThrowExceptionOnFailureWhenFailFastIsEnabled() { NormalizedSource source = new NamedSecretNormalizedSource("secret", "default", true, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, "default", - new MockEnvironment()); + new MockEnvironment(), true, NAMESPACED_BATCH_READ); assertThatThrownBy(() -> new KubernetesClientSecretsPropertySource(context)) .isInstanceOf(IllegalStateException.class) @@ -224,7 +226,7 @@ void constructorShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() { NormalizedSource source = new NamedSecretNormalizedSource("secret", "db-secret", false, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, "default", - new MockEnvironment()); + new MockEnvironment(), true, NAMESPACED_BATCH_READ); assertThatNoException().isThrownBy((() -> new KubernetesClientSecretsPropertySource(context))); verify(getRequestedFor(urlEqualTo(API))); diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/LabeledConfigMapContextToSourceDataProviderTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/LabeledConfigMapContextToSourceDataProviderNamespacedBatchReadTests.java similarity index 94% rename from spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/LabeledConfigMapContextToSourceDataProviderTests.java rename to spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/LabeledConfigMapContextToSourceDataProviderNamespacedBatchReadTests.java index deb1c28847..3b5d2682bf 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/LabeledConfigMapContextToSourceDataProviderTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/LabeledConfigMapContextToSourceDataProviderNamespacedBatchReadTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2022 the original author or authors. + * Copyright 2013-2024 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. @@ -32,6 +32,7 @@ import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; import io.kubernetes.client.util.ClientBuilder; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -55,7 +56,9 @@ * @author wind57 */ @ExtendWith(OutputCaptureExtension.class) -class LabeledConfigMapContextToSourceDataProviderTests { +class LabeledConfigMapContextToSourceDataProviderNamespacedBatchReadTests { + + private static final boolean NAMESPACED_BATCH_READ = true; private static final Map LABELS = new LinkedHashMap<>(); @@ -87,7 +90,12 @@ static void setup() { @AfterEach void afterEach() { WireMock.reset(); - new KubernetesClientConfigMapsCache().discardAll(); + new KubernetesClientSourcesNamespaceBatched().discardConfigMaps(); + } + + @AfterAll + static void afterAll() { + WireMock.shutdownServer(); } /** @@ -111,7 +119,7 @@ void singleConfigMapMatchAgainstLabels() { NormalizedSource source = new LabeledConfigMapNormalizedSource(NAMESPACE, LABELS, true, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -161,7 +169,7 @@ void twoConfigMapsMatchAgainstLabels() { NormalizedSource source = new LabeledConfigMapNormalizedSource(NAMESPACE, RED_LABEL, true, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -193,7 +201,7 @@ void configMapNoMatch() { NormalizedSource source = new LabeledConfigMapNormalizedSource(NAMESPACE, BLUE_LABEL, true, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -226,7 +234,7 @@ void namespaceMatch() { String wrongNamespace = NAMESPACE + "nope"; NormalizedSource source = new LabeledConfigMapNormalizedSource(wrongNamespace, LABELS, true, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -258,7 +266,7 @@ void testWithPrefix() { ConfigUtils.Prefix mePrefix = ConfigUtils.findPrefix("me", false, false, "irrelevant"); NormalizedSource source = new LabeledConfigMapNormalizedSource(NAMESPACE, BLUE_LABEL, true, mePrefix, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -304,7 +312,7 @@ void testTwoConfigmapsWithPrefix() { NormalizedSource source = new LabeledConfigMapNormalizedSource(NAMESPACE, BLUE_LABEL, true, ConfigUtils.Prefix.DELAYED, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -357,7 +365,7 @@ void searchWithLabelsNoConfigmapsFound() { NormalizedSource source = new LabeledConfigMapNormalizedSource(NAMESPACE, RED_LABEL, true, ConfigUtils.Prefix.DEFAULT, true); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -396,7 +404,7 @@ void searchWithLabelsOneConfigMapFound() { NormalizedSource source = new LabeledConfigMapNormalizedSource(NAMESPACE, BLUE_LABEL, true, ConfigUtils.Prefix.DEFAULT, true); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -409,9 +417,8 @@ void searchWithLabelsOneConfigMapFound() { /** * two configmaps are deployed: "color-configmap" with label: "{color:blue}" and - * "color-configmap-k8s" with label: "{color:red}". We search by "{color:blue}" and - * find one configmap. Since profiles are enabled, we will also be reading - * "color-configmap-k8s", even if its labels do not match provided ones. + * "color-configmap-k8s" with label: "{color:blue}". We search by "{color:blue}" and + * find them both. */ @Test void searchWithLabelsOneConfigMapFoundAndOneFromProfileFound() { @@ -426,7 +433,7 @@ void searchWithLabelsOneConfigMapFoundAndOneFromProfileFound() { V1ConfigMap two = new V1ConfigMapBuilder() .withMetadata(new V1ObjectMetaBuilder().withName("color-configmap-k8s") - .withLabels(RED_LABEL) + .withLabels(BLUE_LABEL) .withNamespace(NAMESPACE) .build()) .addToData("two", "2") @@ -437,11 +444,11 @@ void searchWithLabelsOneConfigMapFoundAndOneFromProfileFound() { stubCall(configMapList); CoreV1Api api = new CoreV1Api(); MockEnvironment environment = new MockEnvironment(); - environment.setActiveProfiles("k8s"); NormalizedSource source = new LabeledConfigMapNormalizedSource(NAMESPACE, BLUE_LABEL, true, ConfigUtils.Prefix.DELAYED, true); - KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -491,7 +498,7 @@ void searchWithLabelsTwoConfigMapsFoundAndOneFromProfileFound() { V1ConfigMap colorConfigmapK8s = new V1ConfigMapBuilder() .withMetadata(new V1ObjectMetaBuilder().withName("color-configmap-k8s") - .withLabels(RED_LABEL) + .withLabels(BLUE_LABEL) .withNamespace(NAMESPACE) .build()) .addToData("four", "4") @@ -499,7 +506,7 @@ void searchWithLabelsTwoConfigMapsFoundAndOneFromProfileFound() { V1ConfigMap shapeConfigmapK8s = new V1ConfigMapBuilder() .withMetadata(new V1ObjectMetaBuilder().withName("shape-configmap-k8s") - .withLabels(Map.of("shape", "triangle")) + .withLabels(BLUE_LABEL) .withNamespace(NAMESPACE) .build()) .addToData("five", "5") @@ -514,11 +521,11 @@ void searchWithLabelsTwoConfigMapsFoundAndOneFromProfileFound() { stubCall(configMapList); CoreV1Api api = new CoreV1Api(); MockEnvironment environment = new MockEnvironment(); - environment.setActiveProfiles("k8s"); NormalizedSource source = new LabeledConfigMapNormalizedSource(NAMESPACE, BLUE_LABEL, true, ConfigUtils.Prefix.DELAYED, true); - KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -569,7 +576,7 @@ void cache(CapturedOutput output) { NormalizedSource redSource = new LabeledConfigMapNormalizedSource(NAMESPACE, Map.of("color", "red"), false, ConfigUtils.Prefix.DEFAULT, false); KubernetesClientConfigContext redContext = new KubernetesClientConfigContext(api, redSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData redData = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData redSourceData = redData.apply(redContext); @@ -581,7 +588,7 @@ void cache(CapturedOutput output) { NormalizedSource greenSource = new LabeledConfigMapNormalizedSource(NAMESPACE, Map.of("color", "green"), false, ConfigUtils.Prefix.DEFAULT, false); KubernetesClientConfigContext greenContext = new KubernetesClientConfigContext(api, greenSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData greenData = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData greenSourceData = greenData.apply(greenContext); diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/LabeledConfigMapContextToSourceDataProviderNonNamespacedBatchReadTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/LabeledConfigMapContextToSourceDataProviderNonNamespacedBatchReadTests.java new file mode 100644 index 0000000000..ac3452f716 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/LabeledConfigMapContextToSourceDataProviderNonNamespacedBatchReadTests.java @@ -0,0 +1,629 @@ +/* + * Copyright 2013-2024 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.Iterator; +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.V1ConfigMap; +import io.kubernetes.client.openapi.models.V1ConfigMapBuilder; +import io.kubernetes.client.openapi.models.V1ConfigMapList; +import io.kubernetes.client.openapi.models.V1ConfigMapListBuilder; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.util.ClientBuilder; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.kubernetes.commons.config.ConfigUtils; +import org.springframework.cloud.kubernetes.commons.config.LabeledConfigMapNormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +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 + */ +@ExtendWith(OutputCaptureExtension.class) +class LabeledConfigMapContextToSourceDataProviderNonNamespacedBatchReadTests { + + private static final boolean NAMESPACED_BATCH_READ = false; + + private static final Map LABELS = new LinkedHashMap<>(); + + private static final Map RED_LABEL = Map.of("color", "red"); + + private static final Map BLUE_LABEL = Map.of("color", "blue"); + + private static final Map PINK_LABEL = Map.of("color", "pink"); + + 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(); + new KubernetesClientSourcesNamespaceBatched().discardConfigMaps(); + } + + @AfterAll + static void afterAll() { + WireMock.shutdownServer(); + } + + /** + * we have a single config map deployed. it has two labels and these match against our + * queries. + */ + @Test + void singleConfigMapMatchAgainstLabels() { + + V1ConfigMap one = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("test-configmap") + .withLabels(LABELS) + .withNamespace(NAMESPACE) + .build()) + .addToData("name", "value") + .build(); + + V1ConfigMapList configMapList = new V1ConfigMapListBuilder().addToItems(one).build(); + stubCall(configMapList, + "/api/v1/namespaces/default/configmaps?labelSelector=label2%3Dvalue2%26label1%3Dvalue1"); + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new LabeledConfigMapNormalizedSource(NAMESPACE, LABELS, true, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.test-configmap.default"); + Assertions.assertThat(sourceData.sourceData()).containsExactlyInAnyOrderEntriesOf(Map.of("name", "value")); + + } + + /** + * we have three configmaps deployed. two of them have labels that match (color=red), + * one does not (color=blue). + */ + @Test + void twoConfigMapsMatchAgainstLabels() { + + V1ConfigMap redOne = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("red-configmap") + .withLabels(RED_LABEL) + .withNamespace(NAMESPACE) + .build()) + .addToData("colorOne", "really-red") + .build(); + + V1ConfigMap redTwo = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("red-configmap-again") + .withLabels(RED_LABEL) + .withNamespace(NAMESPACE) + .build()) + .addToData("colorTwo", "really-red-again") + .build(); + + V1ConfigMap blue = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("blue-configmap") + .withLabels(BLUE_LABEL) + .withNamespace(NAMESPACE) + .build()) + .addToData("color", "blue") + .build(); + + V1ConfigMapList configMapList = new V1ConfigMapList().addItemsItem(redOne) + .addItemsItem(redTwo) + .addItemsItem(blue); + + stubCall(configMapList, "/api/v1/namespaces/default/configmaps?labelSelector=color%3Dred"); + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new LabeledConfigMapNormalizedSource(NAMESPACE, RED_LABEL, true, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red-configmap.red-configmap-again.default"); + Assertions.assertThat(sourceData.sourceData()).hasSize(2); + Assertions.assertThat(sourceData.sourceData().get("colorOne")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData().get("colorTwo")).isEqualTo("really-red-again"); + + } + + /** + * one configmap deployed (pink), does not match our query (blue). + */ + @Test + void configMapNoMatch() { + + V1ConfigMap one = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("pink-configmap") + .withLabels(PINK_LABEL) + .withNamespace(NAMESPACE) + .build()) + .addToData("color", "pink") + .build(); + V1ConfigMapList configMapList = new V1ConfigMapList().addItemsItem(one); + + // pink returns one + stubCall(configMapList, "/api/v1/namespaces/default/configmaps?labelSelector=color%3Dpink"); + + // blue returns none + stubCall(new V1ConfigMapList(), "/api/v1/namespaces/default/configmaps?labelSelector=color%3Dblue"); + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new LabeledConfigMapNormalizedSource(NAMESPACE, BLUE_LABEL, true, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.color.default"); + Assertions.assertThat(sourceData.sourceData()).isEmpty(); + + } + + /** + * LabeledConfigMapContextToSourceDataProvider 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() { + V1ConfigMap one = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("test-configmap") + .withLabels(LABELS) + .withNamespace(NAMESPACE) + .build()) + .addToData("name", "value") + .build(); + V1ConfigMapList configMapList = new V1ConfigMapList().addItemsItem(one); + stubCall(configMapList, + "/api/v1/namespaces/default/configmaps?labelSelector=label2%3Dvalue2%26label1%3Dvalue1"); + + CoreV1Api api = new CoreV1Api(); + + String wrongNamespace = NAMESPACE + "nope"; + NormalizedSource source = new LabeledConfigMapNormalizedSource(wrongNamespace, LABELS, true, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.test-configmap.default"); + Assertions.assertThat(sourceData.sourceData()).containsExactlyInAnyOrderEntriesOf(Map.of("name", "value")); + } + + /** + * one configmap with name : "blue-configmap" and labels "color=blue" is deployed. we + * search it with the same labels, find it, and assert that name of the SourceData (it + * must use its name, not its labels) and values in the SourceData must be prefixed + * (since we have provided an explicit prefix). + */ + @Test + void testWithPrefix() { + V1ConfigMap one = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("blue-configmap") + .withLabels(BLUE_LABEL) + .withNamespace(NAMESPACE) + .build()) + .addToData("what-color", "blue-color") + .build(); + V1ConfigMapList configMapList = new V1ConfigMapList().addItemsItem(one); + stubCall(configMapList, "/api/v1/namespaces/default/configmaps?labelSelector=color%3Dblue"); + + CoreV1Api api = new CoreV1Api(); + + ConfigUtils.Prefix mePrefix = ConfigUtils.findPrefix("me", false, false, "irrelevant"); + NormalizedSource source = new LabeledConfigMapNormalizedSource(NAMESPACE, BLUE_LABEL, true, mePrefix, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.blue-configmap.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("me.what-color", "blue-color")); + } + + /** + * two configmaps are deployed (name:blue-configmap, name:another-blue-configmap) and + * labels "color=blue" (on both). we search with the same labels, find them, and + * assert that name of the SourceData (it must use its name, not its labels) and + * values in the SourceData must be prefixed (since we have provided a delayed + * prefix). + * + * Also notice that the prefix is made up from both configmap names. + * + */ + @Test + void testTwoConfigmapsWithPrefix() { + + V1ConfigMap one = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("blue-configmap") + .withLabels(BLUE_LABEL) + .withNamespace(NAMESPACE) + .build()) + .addToData("first", "blue") + .build(); + + V1ConfigMap two = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("another-blue-configmap") + .withLabels(BLUE_LABEL) + .withNamespace(NAMESPACE) + .build()) + .addToData("second", "blue") + .build(); + + V1ConfigMapList configMapList = new V1ConfigMapList().addItemsItem(one).addItemsItem(two); + + stubCall(configMapList, "/api/v1/namespaces/default/configmaps?labelSelector=color%3Dblue"); + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new LabeledConfigMapNormalizedSource(NAMESPACE, BLUE_LABEL, true, + ConfigUtils.Prefix.DELAYED, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()) + .isEqualTo("configmap.another-blue-configmap.blue-configmap.default"); + + Map properties = sourceData.sourceData(); + Assertions.assertThat(properties).hasSize(2); + Iterator keys = properties.keySet().iterator(); + String firstKey = keys.next(); + String secondKey = keys.next(); + + if (firstKey.contains("first")) { + Assertions.assertThat("blue-configmap.first").isEqualTo(firstKey); + } + + Assertions.assertThat(secondKey).isEqualTo("another-blue-configmap.second"); + Assertions.assertThat(properties.get(firstKey)).isEqualTo("blue"); + Assertions.assertThat(properties.get(secondKey)).isEqualTo("blue"); + } + + /** + * two configmaps are deployed: "color-configmap" with label: "{color:blue}" and + * "color-configmap-k8s" with no labels. We search by "{color:red}", do not find + * anything and thus have an empty SourceData. + */ + @Test + void searchWithLabelsNoConfigmapsFound() { + + V1ConfigMap one = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("color-configmap") + .withLabels(BLUE_LABEL) + .withNamespace(NAMESPACE) + .build()) + .addToData("one", "1") + .build(); + + V1ConfigMap two = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("color-config-k8s").withNamespace(NAMESPACE).build()) + .addToData("two", "2") + .build(); + + V1ConfigMapList configMapList = new V1ConfigMapList().addItemsItem(one).addItemsItem(two); + + stubCall(configMapList, "/api/v1/namespaces/default/configmaps?labelSelector=color%3Dblue"); + stubCall(new V1ConfigMapList(), "/api/v1/namespaces/default/configmaps?labelSelector=color%3Dred"); + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new LabeledConfigMapNormalizedSource(NAMESPACE, RED_LABEL, true, + ConfigUtils.Prefix.DEFAULT, true); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).isEmpty(); + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.color.default"); + + } + + /** + * two configmaps are deployed: "color-configmap" with label: "{color:blue}" and + * "shape-configmap" with label: "{shape:round}". We search by "{color:blue}" and find + * one configmap. + */ + @Test + void searchWithLabelsOneConfigMapFound() { + + V1ConfigMap one = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("color-configmap") + .withLabels(BLUE_LABEL) + .withNamespace(NAMESPACE) + .build()) + .addToData("one", "1") + .build(); + + V1ConfigMap two = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("shape-configmap").withNamespace(NAMESPACE).build()) + .addToData("two", "2") + .build(); + + V1ConfigMapList configMapListOne = new V1ConfigMapList().addItemsItem(one); + stubCall(configMapListOne, "/api/v1/namespaces/default/configmaps?labelSelector=color%3Dblue"); + + V1ConfigMapList configMapListTwo = new V1ConfigMapList().addItemsItem(one).addItemsItem(two); + stubCall(configMapListTwo, "/api/v1/namespaces/default/configmaps?labelSelector=shape%3Dround"); + + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new LabeledConfigMapNormalizedSource(NAMESPACE, BLUE_LABEL, true, + ConfigUtils.Prefix.DEFAULT, true); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).hasSize(1); + Assertions.assertThat(sourceData.sourceData().get("one")).isEqualTo("1"); + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.color-configmap.default"); + + } + + /** + * two configmaps are deployed: "color-configmap" with label: "{color:blue}" and + * "color-configmap-k8s" with label: "{color:blue}". We search by "{color:blue}" and + * find them both. + */ + @Test + void searchWithLabelsOneConfigMapFoundAndOneFromProfileFound() { + + V1ConfigMap one = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("color-configmap") + .withLabels(BLUE_LABEL) + .withNamespace(NAMESPACE) + .build()) + .addToData("one", "1") + .build(); + + V1ConfigMap two = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("color-configmap-k8s") + .withLabels(BLUE_LABEL) + .withNamespace(NAMESPACE) + .build()) + .addToData("two", "2") + .build(); + + V1ConfigMapList configMapList = new V1ConfigMapList().addItemsItem(one).addItemsItem(two); + stubCall(configMapList, "/api/v1/namespaces/default/configmaps?labelSelector=color%3Dblue"); + + CoreV1Api api = new CoreV1Api(); + MockEnvironment environment = new MockEnvironment(); + + NormalizedSource source = new LabeledConfigMapNormalizedSource(NAMESPACE, BLUE_LABEL, true, + ConfigUtils.Prefix.DELAYED, true); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).hasSize(2); + Assertions.assertThat(sourceData.sourceData().get("color-configmap.one")).isEqualTo("1"); + Assertions.assertThat(sourceData.sourceData().get("color-configmap-k8s.two")).isEqualTo("2"); + Assertions.assertThat(sourceData.sourceName()) + .isEqualTo("configmap.color-configmap.color-configmap-k8s.default"); + + } + + /** + *
+	 *     - configmap "color-configmap" with label "{color:blue}"
+	 *     - configmap "shape-configmap" with labels "{color:blue, shape:round}"
+	 *     - configmap "no-fit" with labels "{tag:no-fit}"
+	 *     - configmap "color-configmap-k8s" with label "{color:red}"
+	 *     - configmap "shape-configmap-k8s" with label "{shape:triangle}"
+	 * 
+ */ + @Test + void searchWithLabelsTwoConfigMapsFoundAndOneFromProfileFound() { + + V1ConfigMap colorConfigMap = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("color-configmap") + .withLabels(BLUE_LABEL) + .withNamespace(NAMESPACE) + .build()) + .addToData("one", "1") + .build(); + + V1ConfigMap shapeConfigmap = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("shape-configmap") + .withLabels(Map.of("color", "blue", "shape", "round")) + .withNamespace(NAMESPACE) + .build()) + .addToData("two", "2") + .build(); + + V1ConfigMap noFit = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("no-fit") + .withLabels(Map.of("tag", "no-fit")) + .withNamespace(NAMESPACE) + .build()) + .addToData("three", "3") + .build(); + + V1ConfigMap colorConfigmapK8s = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("color-configmap-k8s") + .withLabels(BLUE_LABEL) + .withNamespace(NAMESPACE) + .build()) + .addToData("four", "4") + .build(); + + V1ConfigMap shapeConfigmapK8s = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("shape-configmap-k8s") + .withLabels(BLUE_LABEL) + .withNamespace(NAMESPACE) + .build()) + .addToData("five", "5") + .build(); + + V1ConfigMapList configMapList = new V1ConfigMapList().addItemsItem(colorConfigMap) + .addItemsItem(shapeConfigmap) + .addItemsItem(noFit) + .addItemsItem(colorConfigmapK8s) + .addItemsItem(shapeConfigmapK8s); + + stubCall(configMapList, "/api/v1/namespaces/default/configmaps?labelSelector=color%3Dblue"); + CoreV1Api api = new CoreV1Api(); + MockEnvironment environment = new MockEnvironment(); + + NormalizedSource source = new LabeledConfigMapNormalizedSource(NAMESPACE, BLUE_LABEL, true, + ConfigUtils.Prefix.DELAYED, true); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).hasSize(4); + Assertions.assertThat(sourceData.sourceData().get("color-configmap.one")).isEqualTo("1"); + Assertions.assertThat(sourceData.sourceData().get("shape-configmap.two")).isEqualTo("2"); + Assertions.assertThat(sourceData.sourceData().get("color-configmap-k8s.four")).isEqualTo("4"); + Assertions.assertThat(sourceData.sourceData().get("shape-configmap-k8s.five")).isEqualTo("5"); + + Assertions.assertThat(sourceData.sourceName()) + .isEqualTo("configmap.color-configmap.color-configmap-k8s.shape-configmap.shape-configmap-k8s.default"); + + } + + /** + *
+	 *     - one configmap is deployed with label {"color", "red"}
+	 *     - one configmap is deployed with label {"color", "green"}
+	 *
+	 *     - we first search for "red" and find it, and it is retrieved from the cluster via the client.
+	 * 	   - we then search for the "green" one, and it is not cached.
+	 * 
+ */ + @Test + void nonCache(CapturedOutput output) { + V1ConfigMap red = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "red")) + .withNamespace(NAMESPACE) + .withName("red-configmap") + .build()) + .addToData("color", "red") + .build(); + + V1ConfigMap green = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "green")) + .withNamespace(NAMESPACE) + .withName("green-configmap") + .build()) + .addToData("color", "green") + .build(); + + V1ConfigMapList configMapListRed = new V1ConfigMapList().addItemsItem(red); + stubCall(configMapListRed, "/api/v1/namespaces/default/configmaps?labelSelector=color%3Dred"); + + V1ConfigMapList configMapListGreen = new V1ConfigMapList().addItemsItem(green); + stubCall(configMapListGreen, "/api/v1/namespaces/default/configmaps?labelSelector=color%3Dgreen"); + + CoreV1Api api = new CoreV1Api(); + + NormalizedSource redSource = new LabeledConfigMapNormalizedSource(NAMESPACE, Map.of("color", "red"), false, + ConfigUtils.Prefix.DEFAULT, false); + KubernetesClientConfigContext redContext = new KubernetesClientConfigContext(api, redSource, NAMESPACE, + new MockEnvironment(), false, NAMESPACED_BATCH_READ); + KubernetesClientContextToSourceData redData = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData redSourceData = redData.apply(redContext); + + Assertions.assertThat(redSourceData.sourceData()).hasSize(1); + Assertions.assertThat(redSourceData.sourceData().get("color")).isEqualTo("red"); + Assertions.assertThat(redSourceData.sourceName()).isEqualTo("configmap.red-configmap.default"); + + Assertions.assertThat(output.getAll()) + .doesNotContain("Loaded all config maps in namespace '" + NAMESPACE + "'"); + Assertions.assertThat(output.getAll()).contains("Will read individual configmaps in namespace"); + + NormalizedSource greenSource = new LabeledConfigMapNormalizedSource(NAMESPACE, Map.of("color", "green"), false, + ConfigUtils.Prefix.DEFAULT, false); + KubernetesClientConfigContext greenContext = new KubernetesClientConfigContext(api, greenSource, NAMESPACE, + new MockEnvironment(), false, NAMESPACED_BATCH_READ); + KubernetesClientContextToSourceData greenData = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData greenSourceData = greenData.apply(greenContext); + + Assertions.assertThat(greenSourceData.sourceData()).hasSize(1); + Assertions.assertThat(greenSourceData.sourceData().get("color")).isEqualTo("green"); + Assertions.assertThat(greenSourceData.sourceName()).isEqualTo("configmap.green-configmap.default"); + + // meaning there is a single entry with such a log statement + String[] out = output.getAll().split("Loaded all config maps in namespace"); + Assertions.assertThat(out.length).isEqualTo(1); + + // meaning that the second read was done from the cache + out = output.getAll().split("Will read individual configmaps in namespace"); + Assertions.assertThat(out.length).isEqualTo(3); + } + + private void stubCall(V1ConfigMapList configMapList, String path) { + stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(configMapList)))); + } + +} 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/LabeledSecretContextToSourceDataProviderNamespacedBatchReadTests.java similarity index 92% rename from spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/LabeledSecretContextToSourceDataProviderTests.java rename to spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/LabeledSecretContextToSourceDataProviderNamespacedBatchReadTests.java index 6cbf1766d6..a4467427d1 100644 --- 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/LabeledSecretContextToSourceDataProviderNamespacedBatchReadTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2022 the original author or authors. + * Copyright 2013-2024 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. @@ -34,6 +34,7 @@ import io.kubernetes.client.openapi.models.V1SecretList; import io.kubernetes.client.util.ClientBuilder; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -57,7 +58,9 @@ * @author wind57 */ @ExtendWith(OutputCaptureExtension.class) -class LabeledSecretContextToSourceDataProviderTests { +class LabeledSecretContextToSourceDataProviderNamespacedBatchReadTests { + + private static final boolean NAMESPACED_BATCH_READ = true; private static final Map LABELS = new LinkedHashMap<>(); @@ -85,7 +88,12 @@ static void setup() { @AfterEach void afterEach() { WireMock.reset(); - new KubernetesClientSecretsCache().discardAll(); + new KubernetesClientSourcesNamespaceBatched().discardSecrets(); + } + + @AfterAll + static void afterAll() { + WireMock.shutdownServer(); } /** @@ -108,9 +116,9 @@ void noMatch() { // blue does not match red NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, - Collections.singletonMap("color", "blue"), false, false); + Collections.singletonMap("color", "blue"), false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -136,9 +144,9 @@ void singleSecretMatchAgainstLabels() { stubCall(secretList); CoreV1Api api = new CoreV1Api(); - NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, LABELS, false, false); + NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, LABELS, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -168,9 +176,9 @@ void twoSecretsMatchAgainstLabels() { stubCall(secretList); CoreV1Api api = new CoreV1Api(); - NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, RED_LABEL, false, false); + NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, RED_LABEL, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -193,9 +201,9 @@ void namespaceMatch() { stubCall(secretList); CoreV1Api api = new CoreV1Api(); - NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE + "nope", LABELS, false, false); + NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE + "nope", LABELS, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -226,10 +234,9 @@ void testWithPrefix() { CoreV1Api api = new CoreV1Api(); ConfigUtils.Prefix prefix = ConfigUtils.findPrefix("me", false, false, null); - NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "blue"), false, prefix, - false); + NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "blue"), false, prefix); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -272,9 +279,9 @@ void testTwoSecretsWithPrefix() { CoreV1Api api = new CoreV1Api(); NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "blue"), false, - ConfigUtils.Prefix.DELAYED, false); + ConfigUtils.Prefix.DELAYED); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -304,7 +311,7 @@ void testTwoSecretsWithPrefix() { /** * two secrets are deployed: secret "color-secret" with label: "{color:blue}" and * "shape-secret" with label: "{shape:round}". We search by "{color:blue}" and find - * one secret. profile based sources are enabled, but it has no effect. + * one secret. */ @Test void searchWithLabelsOneSecretFound() { @@ -331,9 +338,9 @@ void searchWithLabelsOneSecretFound() { CoreV1Api api = new CoreV1Api(); NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "blue"), false, - ConfigUtils.Prefix.DEFAULT, true); + ConfigUtils.Prefix.DEFAULT); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -346,9 +353,8 @@ void searchWithLabelsOneSecretFound() { /** * two secrets are deployed: secret "color-secret" with label: "{color:blue}" and - * "color-secret-k8s" with label: "{color:red}". We search by "{color:blue}" and find - * one secret. Since profiles are enabled, we will also be reading "color-secret-k8s", - * even if its labels do not match provided ones. + * "color-secret-k8s" with label: "{color:blue}". We search by "{color:blue}" and find + * both. */ @Test void searchWithLabelsOneSecretFoundAndOneFromProfileFound() { @@ -362,7 +368,7 @@ void searchWithLabelsOneSecretFoundAndOneFromProfileFound() { .build(); V1Secret shapeSecret = new V1SecretBuilder() - .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "red")) + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue")) .withNamespace(NAMESPACE) .withName("color-secret-k8s") .build()) @@ -374,11 +380,11 @@ void searchWithLabelsOneSecretFoundAndOneFromProfileFound() { stubCall(secretList); CoreV1Api api = new CoreV1Api(); MockEnvironment environment = new MockEnvironment(); - environment.setActiveProfiles("k8s"); NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "blue"), false, - ConfigUtils.Prefix.DELAYED, true); - KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment); + ConfigUtils.Prefix.DELAYED); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -427,7 +433,7 @@ void searchWithLabelsTwoSecretsFoundAndOneFromProfileFound() { .build(); V1Secret colorSecretK8s = new V1SecretBuilder() - .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "red")) + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue")) .withNamespace(NAMESPACE) .withName("color-secret-k8s") .build()) @@ -435,7 +441,7 @@ void searchWithLabelsTwoSecretsFoundAndOneFromProfileFound() { .build(); V1Secret shapeSecretK8s = new V1SecretBuilder() - .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("shape", "triangle")) + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue")) .withNamespace(NAMESPACE) .withName("shape-secret-k8s") .build()) @@ -451,11 +457,11 @@ void searchWithLabelsTwoSecretsFoundAndOneFromProfileFound() { stubCall(secretList); CoreV1Api api = new CoreV1Api(); MockEnvironment environment = new MockEnvironment(); - environment.setActiveProfiles("k8s"); NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "blue"), false, - ConfigUtils.Prefix.DELAYED, true); - KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment); + ConfigUtils.Prefix.DELAYED); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -490,9 +496,9 @@ void testYaml() { CoreV1Api api = new CoreV1Api(); NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "blue"), false, - ConfigUtils.Prefix.DEFAULT, true); + ConfigUtils.Prefix.DEFAULT); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -535,9 +541,9 @@ void cache(CapturedOutput output) { CoreV1Api api = new CoreV1Api(); NormalizedSource redSource = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "red"), false, - ConfigUtils.Prefix.DEFAULT, false); + ConfigUtils.Prefix.DEFAULT); KubernetesClientConfigContext redContext = new KubernetesClientConfigContext(api, redSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData redData = new LabeledSecretContextToSourceDataProvider().get(); SourceData redSourceData = redData.apply(redContext); @@ -547,9 +553,9 @@ void cache(CapturedOutput output) { Assertions.assertThat(output.getOut()).contains("Loaded all secrets in namespace '" + NAMESPACE + "'"); NormalizedSource greenSource = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "green"), false, - ConfigUtils.Prefix.DEFAULT, false); + ConfigUtils.Prefix.DEFAULT); KubernetesClientConfigContext greenContext = new KubernetesClientConfigContext(api, greenSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData greenData = new LabeledSecretContextToSourceDataProvider().get(); SourceData greenSourceData = greenData.apply(greenContext); diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/LabeledSecretContextToSourceDataProviderNonNamespacedBatchReadTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/LabeledSecretContextToSourceDataProviderNonNamespacedBatchReadTests.java new file mode 100644 index 0000000000..5598a729c4 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/LabeledSecretContextToSourceDataProviderNonNamespacedBatchReadTests.java @@ -0,0 +1,587 @@ +/* + * Copyright 2013-2024 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.Iterator; +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.V1Secret; +import io.kubernetes.client.openapi.models.V1SecretBuilder; +import io.kubernetes.client.openapi.models.V1SecretList; +import io.kubernetes.client.util.ClientBuilder; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.kubernetes.commons.config.ConfigUtils; +import org.springframework.cloud.kubernetes.commons.config.LabeledSecretNormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +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 + */ +@ExtendWith(OutputCaptureExtension.class) +public class LabeledSecretContextToSourceDataProviderNonNamespacedBatchReadTests { + + private static final boolean NAMESPACED_BATCH_READ = false; + + 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(); + new KubernetesClientSourcesNamespaceBatched().discardSecrets(); + } + + @AfterAll + static void afterAll() { + WireMock.shutdownServer(); + } + + /** + * we have a single secret deployed. it does not match our query. + */ + @Test + void noMatch() { + + V1Secret red = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Collections.singletonMap("color", "red")) + .withNamespace(NAMESPACE) + .withName("red-secret") + .build()) + .addToData("color", Base64.getEncoder().encode("really-red".getBytes())) + .build(); + V1SecretList secretList = new V1SecretList().addItemsItem(red); + + stubCall(secretList, "/api/v1/namespaces/default/secrets?labelSelector=color%3Dred"); + stubCall(new V1SecretList(), "/api/v1/namespaces/default/secrets?labelSelector=color%3Dblue"); + CoreV1Api api = new CoreV1Api(); + + // blue does not match red + NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "blue"), false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.color.default"); + Assertions.assertThat(sourceData.sourceData()).isEmpty(); + + } + + /** + * we have a single secret deployed. it has two labels and these match against our + * queries. + */ + @Test + void singleSecretMatchAgainstLabels() { + + V1Secret red = new V1SecretBuilder().withMetadata( + new V1ObjectMetaBuilder().withLabels(LABELS).withNamespace(NAMESPACE).withName("test-secret").build()) + .addToData("color", "really-red".getBytes()) + .build(); + V1SecretList secretList = new V1SecretList().addItemsItem(red); + + stubCall(secretList, "/api/v1/namespaces/default/secrets?labelSelector=label2%3Dvalue2%26label1%3Dvalue1"); + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, LABELS, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.test-secret.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("color", "really-red")); + + } + + /** + * we have two secrets deployed. both of them have labels that match (color=red). + */ + @Test + void twoSecretsMatchAgainstLabels() { + + V1Secret one = new V1SecretBuilder().withMetadata( + new V1ObjectMetaBuilder().withLabels(RED_LABEL).withNamespace(NAMESPACE).withName("color-one").build()) + .addToData("colorOne", "really-red-one".getBytes()) + .build(); + + V1Secret two = new V1SecretBuilder().withMetadata( + new V1ObjectMetaBuilder().withLabels(RED_LABEL).withNamespace(NAMESPACE).withName("color-two").build()) + .addToData("colorTwo", "really-red-two".getBytes()) + .build(); + + V1SecretList secretList = new V1SecretList().addItemsItem(one).addItemsItem(two); + stubCall(secretList, "/api/v1/namespaces/default/secrets?labelSelector=color%3Dred"); + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, RED_LABEL, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.color-one.color-two.default"); + Assertions.assertThat(sourceData.sourceData().size()).isEqualTo(2); + Assertions.assertThat(sourceData.sourceData().get("colorOne")).isEqualTo("really-red-one"); + Assertions.assertThat(sourceData.sourceData().get("colorTwo")).isEqualTo("really-red-two"); + + } + + @Test + void namespaceMatch() { + V1Secret one = new V1SecretBuilder().withMetadata( + new V1ObjectMetaBuilder().withLabels(LABELS).withNamespace(NAMESPACE).withName("test-secret").build()) + .addToData("color", "really-red".getBytes()) + .build(); + V1SecretList secretList = new V1SecretList().addItemsItem(one); + + stubCall(secretList, "/api/v1/namespaces/default/secrets?labelSelector=label2%3Dvalue2%26label1%3Dvalue1"); + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE + "nope", LABELS, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.test-secret.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("color", "really-red")); + } + + /** + * one secret with name : "blue-secret" and labels "color=blue" is deployed. we search + * it with the same labels, find it, and assert that name of the SourceData (it must + * use its name, not its labels) and values in the SourceData must be prefixed (since + * we have provided an explicit prefix). + */ + @Test + void testWithPrefix() { + + V1Secret one = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue")) + .withNamespace(NAMESPACE) + .withName("blue-secret") + .build()) + .addToData("what-color", "blue-color".getBytes()) + .build(); + V1SecretList secretList = new V1SecretList().addItemsItem(one); + + stubCall(secretList, "/api/v1/namespaces/default/secrets?labelSelector=color%3Dblue"); + CoreV1Api api = new CoreV1Api(); + + ConfigUtils.Prefix prefix = ConfigUtils.findPrefix("me", false, false, null); + NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "blue"), false, prefix); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.blue-secret.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("me.what-color", "blue-color")); + } + + /** + * two secrets are deployed (name:blue-secret, name:another-blue-secret) and labels + * "color=blue" (on both). we search with the same labels, find them, and assert that + * name of the SourceData (it must use its name, not its labels) and values in the + * SourceData must be prefixed (since we have provided a delayed prefix). + * + * Also notice that the prefix is made up from both secret names. + * + */ + @Test + void testTwoSecretsWithPrefix() { + + V1Secret one = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue")) + .withNamespace(NAMESPACE) + .withName("blue-secret") + .build()) + .addToData("first", "blue".getBytes()) + .build(); + + V1Secret two = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue")) + .withNamespace(NAMESPACE) + .withName("another-blue-secret") + .build()) + .addToData("second", "blue".getBytes()) + .build(); + + V1SecretList secretList = new V1SecretList().addItemsItem(one).addItemsItem(two); + + stubCall(secretList, "/api/v1/namespaces/default/secrets?labelSelector=color%3Dblue"); + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "blue"), false, + ConfigUtils.Prefix.DELAYED); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + // maps don't have a defined order, so assert components separately + Assertions.assertThat(sourceData.sourceName().length()).isEqualTo(46); + Assertions.assertThat(sourceData.sourceName()).contains("secret"); + Assertions.assertThat(sourceData.sourceName()).contains("blue-secret"); + Assertions.assertThat(sourceData.sourceName()).contains("another-blue-secret"); + Assertions.assertThat(sourceData.sourceName()).contains("default"); + + Map properties = sourceData.sourceData(); + Assertions.assertThat(properties).hasSize(2); + Iterator keys = properties.keySet().iterator(); + String firstKey = keys.next(); + String secondKey = keys.next(); + + if (firstKey.contains("first")) { + Assertions.assertThat(firstKey).isEqualTo("blue-secret.first"); + } + + Assertions.assertThat(secondKey).isEqualTo("another-blue-secret.second"); + Assertions.assertThat(properties.get(firstKey)).isEqualTo("blue"); + Assertions.assertThat(properties.get(secondKey)).isEqualTo("blue"); + } + + /** + * two secrets are deployed: secret "color-secret" with label: "{color:blue}" and + * "shape-secret" with label: "{shape:round}". We search by "{color:blue}" and find + * one secret. + */ + @Test + void searchWithLabelsOneSecretFound() { + + V1Secret colorSecret = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue")) + .withNamespace(NAMESPACE) + .withName("color-secret") + .build()) + .addToData("one", "1".getBytes()) + .build(); + + V1Secret shapeSecret = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("shape", "round")) + .withNamespace(NAMESPACE) + .withName("shape-secret") + .build()) + .addToData("two", "2".getBytes()) + .build(); + + V1SecretList secretList = new V1SecretList().addItemsItem(colorSecret).addItemsItem(shapeSecret); + + stubCall(secretList, "/api/v1/namespaces/default/secrets?labelSelector=color%3Dblue"); + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "blue"), false, + ConfigUtils.Prefix.DEFAULT); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).hasSize(1); + Assertions.assertThat(sourceData.sourceData().get("one")).isEqualTo("1"); + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.color-secret.default"); + + } + + /** + * two secrets are deployed: secret "color-secret" with label: "{color:blue}" and + * "color-secret-k8s" with label: "{color:blue}". We search by "{color:blue}" and find + * both. + */ + @Test + void searchWithLabelsOneSecretFoundAndOneFromProfileFound() { + + V1Secret colorSecret = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue")) + .withNamespace(NAMESPACE) + .withName("blue-ocean-secret") + .build()) + .addToData("one", "1".getBytes()) + .build(); + + V1Secret shapeSecret = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue")) + .withNamespace(NAMESPACE) + .withName("blue-sky-secret") + .build()) + .addToData("two", "2".getBytes()) + .build(); + + V1SecretList secretList = new V1SecretList().addItemsItem(colorSecret).addItemsItem(shapeSecret); + + stubCall(secretList, "/api/v1/namespaces/default/secrets?labelSelector=color%3Dblue"); + CoreV1Api api = new CoreV1Api(); + MockEnvironment environment = new MockEnvironment(); + + NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "blue"), false, + ConfigUtils.Prefix.DELAYED); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).hasSize(2); + Assertions.assertThat(sourceData.sourceData().get("blue-ocean-secret.one")).isEqualTo("1"); + Assertions.assertThat(sourceData.sourceData().get("blue-sky-secret.two")).isEqualTo("2"); + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.blue-ocean-secret.blue-sky-secret.default"); + + } + + /** + *
+	 *     - secret "color-secret" with label "{color:blue}"
+	 *     - secret "shape-secret" with labels "{color:blue, shape:round}"
+	 *     - secret "no-fit" with labels "{tag:no-fit}"
+	 *     - secret "color-secret-k8s" with label "{color:red}"
+	 *     - secret "shape-secret-k8s" with label "{shape:triangle}"
+	 * 
+ */ + @Test + void searchWithLabelsTwoSecretsFoundAndOneFromProfileFound() { + + V1Secret colorSecret = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue")) + .withNamespace(NAMESPACE) + .withName("color-secret") + .build()) + .addToData("one", "1".getBytes()) + .build(); + + V1Secret shapeSecret = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue", "shape", "round")) + .withNamespace(NAMESPACE) + .withName("shape-secret") + .build()) + .addToData("two", "2".getBytes()) + .build(); + + V1Secret noFit = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("tag", "no-fit")) + .withNamespace(NAMESPACE) + .withName("no-fit") + .build()) + .addToData("three", "3".getBytes()) + .build(); + + V1Secret colorSecretK8s = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue")) + .withNamespace(NAMESPACE) + .withName("color-secret-k8s") + .build()) + .addToData("four", "4".getBytes()) + .build(); + + V1Secret shapeSecretK8s = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue")) + .withNamespace(NAMESPACE) + .withName("shape-secret-k8s") + .build()) + .addToData("five", "5".getBytes()) + .build(); + + V1SecretList secretList = new V1SecretList().addItemsItem(colorSecret) + .addItemsItem(shapeSecret) + .addItemsItem(noFit) + .addItemsItem(colorSecretK8s) + .addItemsItem(shapeSecretK8s); + + stubCall(secretList, "/api/v1/namespaces/default/secrets?labelSelector=color%3Dblue"); + CoreV1Api api = new CoreV1Api(); + MockEnvironment environment = new MockEnvironment(); + + NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "blue"), false, + ConfigUtils.Prefix.DELAYED); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).hasSize(4); + Assertions.assertThat(sourceData.sourceData().get("color-secret.one")).isEqualTo("1"); + Assertions.assertThat(sourceData.sourceData().get("shape-secret.two")).isEqualTo("2"); + Assertions.assertThat(sourceData.sourceData().get("color-secret-k8s.four")).isEqualTo("4"); + Assertions.assertThat(sourceData.sourceData().get("shape-secret-k8s.five")).isEqualTo("5"); + + Assertions.assertThat(sourceData.sourceName()) + .isEqualTo("secret.color-secret.color-secret-k8s.shape-secret.shape-secret-k8s.default"); + + } + + /** + * yaml/properties gets special treatment + */ + @Test + void testYaml() { + V1Secret colorSecret = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue")) + .withNamespace(NAMESPACE) + .withName("color-secret") + .build()) + .addToData("test.yaml", "color: blue".getBytes()) + .build(); + + V1SecretList secretList = new V1SecretList().addItemsItem(colorSecret); + + stubCall(secretList, "/api/v1/namespaces/default/secrets?labelSelector=color%3Dblue"); + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "blue"), false, + ConfigUtils.Prefix.DEFAULT); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).hasSize(1); + Assertions.assertThat(sourceData.sourceData().get("color")).isEqualTo("blue"); + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.color-secret.default"); + } + + /** + *
+	 *     - one secret is deployed with label {"color", "red"}
+	 *     - one secret is deployed with label {"color", "green"}
+	 *
+	 *     - we first search for "red" and find it, and it is retrieved from the cluster via the client.
+	 * 	   - we then search for the "green" one, and it is not retrieved from the cache.
+	 * 
+ */ + @Test + void nonCache(CapturedOutput output) { + V1Secret red = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "red")) + .withNamespace(NAMESPACE) + .withName("red") + .build()) + .addToData("color", "red".getBytes()) + .build(); + + V1Secret green = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "green")) + .withNamespace(NAMESPACE) + .withName("green") + .build()) + .addToData("color", "green".getBytes()) + .build(); + + V1SecretList secretListRed = new V1SecretList().addItemsItem(red); + stubCall(secretListRed, "/api/v1/namespaces/default/secrets?labelSelector=color%3Dred"); + + V1SecretList secretListGreen = new V1SecretList().addItemsItem(green); + stubCall(secretListGreen, "/api/v1/namespaces/default/secrets?labelSelector=color%3Dgreen"); + + CoreV1Api api = new CoreV1Api(); + + NormalizedSource redSource = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "red"), false, + ConfigUtils.Prefix.DEFAULT); + KubernetesClientConfigContext redContext = new KubernetesClientConfigContext(api, redSource, NAMESPACE, + new MockEnvironment(), false, NAMESPACED_BATCH_READ); + KubernetesClientContextToSourceData redData = new LabeledSecretContextToSourceDataProvider().get(); + SourceData redSourceData = redData.apply(redContext); + + Assertions.assertThat(redSourceData.sourceData()).hasSize(1); + Assertions.assertThat(redSourceData.sourceData().get("color")).isEqualTo("red"); + Assertions.assertThat(redSourceData.sourceName()).isEqualTo("secret.red.default"); + + Assertions.assertThat(output.getAll()).doesNotContain("Loaded all secrets in namespace '" + NAMESPACE + "'"); + Assertions.assertThat(output.getAll()).contains("Will read individual secrets in namespace"); + + NormalizedSource greenSource = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "green"), false, + ConfigUtils.Prefix.DEFAULT); + KubernetesClientConfigContext greenContext = new KubernetesClientConfigContext(api, greenSource, NAMESPACE, + new MockEnvironment(), false, NAMESPACED_BATCH_READ); + KubernetesClientContextToSourceData greenData = new LabeledSecretContextToSourceDataProvider().get(); + SourceData greenSourceData = greenData.apply(greenContext); + + Assertions.assertThat(greenSourceData.sourceData()).hasSize(1); + Assertions.assertThat(greenSourceData.sourceData().get("color")).isEqualTo("green"); + Assertions.assertThat(greenSourceData.sourceName()).isEqualTo("secret.green.default"); + + // meaning there is a single entry with such a log statement + String[] out = output.getAll().split("Loaded all secrets in namespace"); + Assertions.assertThat(out.length).isEqualTo(1); + + // meaning that the second read was done from the cache + out = output.getAll().split("Will read individual secrets in namespace"); + Assertions.assertThat(out.length).isEqualTo(3); + } + + private void stubCall(V1SecretList configMapList, String path) { + stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(configMapList)))); + } + +} 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/NamedConfigMapContextToSourceDataProviderNamespacedBatchReadTests.java similarity index 94% rename from spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/NamedConfigMapContextToSourceDataProviderTests.java rename to spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/NamedConfigMapContextToSourceDataProviderNamespacedBatchReadTests.java index 3a4f193e30..fd5b96fea6 100644 --- 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/NamedConfigMapContextToSourceDataProviderNamespacedBatchReadTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2022 the original author or authors. + * Copyright 2013-2024 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. @@ -31,6 +31,7 @@ import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; import io.kubernetes.client.util.ClientBuilder; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -53,7 +54,9 @@ * @author wind57 */ @ExtendWith(OutputCaptureExtension.class) -class NamedConfigMapContextToSourceDataProviderTests { +class NamedConfigMapContextToSourceDataProviderNamespacedBatchReadTests { + + private static final boolean NAMESPACED_BATCH_READ = true; private static final String NAMESPACE = "default"; @@ -82,7 +85,12 @@ static void setup() { @AfterEach void afterEach() { WireMock.reset(); - new KubernetesClientConfigMapsCache().discardAll(); + new KubernetesClientSourcesNamespaceBatched().discardConfigMaps(); + } + + @AfterAll + static void afterAll() { + WireMock.shutdownServer(); } /** @@ -104,7 +112,7 @@ void noMatch() { NormalizedSource source = new NamedConfigMapNormalizedSource(BLUE_CONFIG_MAP_NAME, NAMESPACE, true, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), true, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -134,7 +142,7 @@ void match() { NormalizedSource source = new NamedConfigMapNormalizedSource(RED_CONFIG_MAP_NAME, NAMESPACE, true, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), true, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -147,8 +155,6 @@ void match() { /** *
 	 *     - two configmaps deployed : "red" and "red-with-profile".
-	 *     - "red" is matched directly, "red-with-profile" is matched because we have an active profile
-	 *       "active-profile"
 	 * 
*/ @Test @@ -173,14 +179,13 @@ void matchIncludeSingleProfile() { MockEnvironment environment = new MockEnvironment(); environment.addActiveProfile("with-profile"); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, - true); + false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red.red-with-profile.default"); - Assertions.assertThat(sourceData.sourceData()) - .containsExactlyInAnyOrderEntriesOf(Map.of("color", "really-red", "taste", "mango")); + Assertions.assertThat(sourceData.sourceData()).containsExactlyInAnyOrderEntriesOf(Map.of("taste", "mango")); } @@ -215,7 +220,8 @@ void matchIncludeSingleProfileWithPrefix() { true); MockEnvironment environment = new MockEnvironment(); environment.setActiveProfiles("with-profile"); - KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + true, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -270,7 +276,8 @@ void matchIncludeTwoProfilesWithPrefix() { true); MockEnvironment environment = new MockEnvironment(); environment.setActiveProfiles("with-taste", "with-shape"); - KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + true, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -304,7 +311,7 @@ void matchWithName() { ConfigUtils.Prefix prefix = ConfigUtils.findPrefix("some", false, false, null); NormalizedSource source = new NamedConfigMapNormalizedSource("application", NAMESPACE, true, prefix, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), true, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -335,7 +342,7 @@ void namespaceMatch() { String wrongNamespace = NAMESPACE + "nope"; NormalizedSource source = new NamedConfigMapNormalizedSource(RED_CONFIG_MAP_NAME, wrongNamespace, true, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), true, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -362,7 +369,7 @@ void testSingleYaml() { NormalizedSource source = new NamedConfigMapNormalizedSource(RED_CONFIG_MAP_NAME, NAMESPACE, true, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), true, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -394,7 +401,8 @@ void testCorrectNameWithProfile() { environment.setActiveProfiles("k8s"); NormalizedSource source = new NamedConfigMapNormalizedSource("one", NAMESPACE, true, true); - KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + true, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -433,7 +441,7 @@ void cache(CapturedOutput output) { NormalizedSource redSource = new NamedConfigMapNormalizedSource("red", NAMESPACE, true, false); KubernetesClientConfigContext redContext = new KubernetesClientConfigContext(api, redSource, NAMESPACE, - environment); + environment, true, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData redData = new NamedConfigMapContextToSourceDataProvider().get(); SourceData redSourceData = redData.apply(redContext); @@ -443,7 +451,7 @@ void cache(CapturedOutput output) { NormalizedSource greenSource = new NamedConfigMapNormalizedSource("green", NAMESPACE, true, true); KubernetesClientConfigContext greenContext = new KubernetesClientConfigContext(api, greenSource, NAMESPACE, - environment); + environment, false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData greenData = new NamedConfigMapContextToSourceDataProvider().get(); SourceData greenSourceData = greenData.apply(greenContext); diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/NamedConfigMapContextToSourceDataProviderNonNamespacedBatchReadTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/NamedConfigMapContextToSourceDataProviderNonNamespacedBatchReadTests.java new file mode 100644 index 0000000000..088fa9343a --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/NamedConfigMapContextToSourceDataProviderNonNamespacedBatchReadTests.java @@ -0,0 +1,468 @@ +/* + * Copyright 2013-2024 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.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.V1ConfigMap; +import io.kubernetes.client.openapi.models.V1ConfigMapBuilder; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.util.ClientBuilder; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.kubernetes.commons.config.ConfigUtils; +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.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 + */ +@ExtendWith(OutputCaptureExtension.class) +class NamedConfigMapContextToSourceDataProviderNonNamespacedBatchReadTests { + + private static final boolean NAMESPACED_BATCH_READ = false; + + private static final String NAMESPACE = "default"; + + private static final String RED_CONFIG_MAP_NAME = "red"; + + private static final String RED_WITH_PROFILE_CONFIG_MAP_NAME = RED_CONFIG_MAP_NAME + "-with-profile"; + + private static final String BLUE_CONFIG_MAP_NAME = "blue"; + + private static final Map COLOR_REALLY_RED = Map.of("color", "really-red"); + + private static final Map TASTE_MANGO = Map.of("taste", "mango"); + + @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(); + new KubernetesClientSourcesNamespaceBatched().discardConfigMaps(); + } + + @AfterAll + static void afterAll() { + WireMock.shutdownServer(); + } + + /** + *
+	 *     one configmap deployed with name "red"
+	 *     we search by name, but for the "blue" one, as such not find it
+	 * 
+ */ + @Test + void noMatch() { + V1ConfigMap redConfigMap = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME).withNamespace(NAMESPACE).build()) + .addToData(COLOR_REALLY_RED) + .build(); + stubCall(redConfigMap, "/api/v1/namespaces/default/configmaps/red"); + + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new NamedConfigMapNormalizedSource(BLUE_CONFIG_MAP_NAME, NAMESPACE, true, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), true, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.blue.default"); + Assertions.assertThat(sourceData.sourceData()).isEmpty(); + + } + + /** + *
+	 *     one configmap deployed with name "red"
+	 *     we search by name, for the "red" one, as such we find it
+	 * 
+ */ + @Test + void match() { + + V1ConfigMap configMap = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME).withNamespace(NAMESPACE).build()) + .addToData(COLOR_REALLY_RED) + .build(); + stubCall(configMap, "/api/v1/namespaces/default/configmaps/red"); + + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new NamedConfigMapNormalizedSource(RED_CONFIG_MAP_NAME, NAMESPACE, true, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), true, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red.default"); + Assertions.assertThat(sourceData.sourceData()).isEqualTo(COLOR_REALLY_RED); + + } + + /** + *
+	 *     - two configmaps deployed : "red" and "red-with-profile".
+	 * 
+ */ + @Test + void matchIncludeSingleProfile() { + + V1ConfigMap red = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME).withNamespace(NAMESPACE).build()) + .addToData(COLOR_REALLY_RED) + .build(); + stubCall(red, "/api/v1/namespaces/default/configmaps/red"); + + V1ConfigMap redWithProfile = new V1ConfigMapBuilder().withMetadata( + new V1ObjectMetaBuilder().withName(RED_WITH_PROFILE_CONFIG_MAP_NAME).withNamespace(NAMESPACE).build()) + .addToData(TASTE_MANGO) + .build(); + stubCall(redWithProfile, "/api/v1/namespaces/default/configmaps/red-with-profile"); + + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new NamedConfigMapNormalizedSource(RED_CONFIG_MAP_NAME, NAMESPACE, true, + ConfigUtils.Prefix.DEFAULT, true, true); + MockEnvironment environment = new MockEnvironment(); + environment.setActiveProfiles("with-profile"); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red.red-with-profile.default.with-profile"); + Assertions.assertThat(sourceData.sourceData().size()).isEqualTo(1); + Assertions.assertThat(sourceData.sourceData().get("taste")).isEqualTo("mango"); + + } + + /** + *
+	 *     - two configmaps deployed : "red" and "red-with-profile".
+	 *     - "red" is matched directly, "red-with-profile" is matched because we have an active profile
+	 *       "active-profile"
+	 *     -  This takes into consideration the prefix, that we explicitly specify.
+	 *        Notice that prefix works for profile based config maps as well.
+	 * 
+ */ + @Test + void matchIncludeSingleProfileWithPrefix() { + + V1ConfigMap red = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME).withNamespace(NAMESPACE).build()) + .addToData(COLOR_REALLY_RED) + .build(); + stubCall(red, "/api/v1/namespaces/default/configmaps/red"); + + V1ConfigMap redWithTaste = new V1ConfigMapBuilder().withMetadata( + new V1ObjectMetaBuilder().withName(RED_WITH_PROFILE_CONFIG_MAP_NAME).withNamespace(NAMESPACE).build()) + .addToData(TASTE_MANGO) + .build(); + stubCall(redWithTaste, "/api/v1/namespaces/default/configmaps/red-with-profile"); + + CoreV1Api api = new CoreV1Api(); + + ConfigUtils.Prefix prefix = ConfigUtils.findPrefix("some", false, false, null); + NormalizedSource source = new NamedConfigMapNormalizedSource(RED_CONFIG_MAP_NAME, NAMESPACE, true, prefix, + true); + MockEnvironment environment = new MockEnvironment(); + environment.setActiveProfiles("with-profile"); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + true, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red.red-with-profile.default"); + Assertions.assertThat(sourceData.sourceData()).hasSize(2); + Assertions.assertThat(sourceData.sourceData().get("some.color")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData().get("some.taste")).isEqualTo("mango"); + + } + + /** + *
+	 *     - three configmaps deployed : "red", "red-with-taste" and "red-with-shape"
+	 *     - "red" is matched directly, the other two are matched because of active profiles
+	 *     -  This takes into consideration the prefix, that we explicitly specify.
+	 *        Notice that prefix works for profile based config maps as well.
+	 * 
+ */ + @Test + void matchIncludeTwoProfilesWithPrefix() { + + V1ConfigMap red = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME).withNamespace(NAMESPACE).build()) + .addToData(COLOR_REALLY_RED) + .build(); + stubCall(red, "/api/v1/namespaces/default/configmaps/red"); + + V1ConfigMap redWithTaste = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME + "-with-taste") + .withNamespace(NAMESPACE) + .withResourceVersion("1") + .build()) + .addToData(TASTE_MANGO) + .build(); + stubCall(redWithTaste, "/api/v1/namespaces/default/configmaps/red-with-taste"); + + V1ConfigMap redWithShape = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME + "-with-shape") + .withNamespace(NAMESPACE) + .build()) + .addToData("shape", "round") + .build(); + stubCall(redWithShape, "/api/v1/namespaces/default/configmaps/red-with-shape"); + + CoreV1Api api = new CoreV1Api(); + + ConfigUtils.Prefix prefix = ConfigUtils.findPrefix("some", false, false, null); + NormalizedSource source = new NamedConfigMapNormalizedSource(RED_CONFIG_MAP_NAME, NAMESPACE, true, prefix, + true); + MockEnvironment environment = new MockEnvironment(); + environment.setActiveProfiles("with-taste", "with-shape"); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + true, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red.red-with-shape.red-with-taste.default"); + Assertions.assertThat(sourceData.sourceData()).hasSize(3); + Assertions.assertThat(sourceData.sourceData().get("some.color")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData().get("some.taste")).isEqualTo("mango"); + Assertions.assertThat(sourceData.sourceData().get("some.shape")).isEqualTo("round"); + + } + + /** + *
+	 * 		proves that an implicit configmap is not going to be generated and read
+	 * 
+ */ + @Test + void matchWithName() { + + V1ConfigMap red = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("application").withNamespace(NAMESPACE).build()) + .addToData("color", "red") + .build(); + stubCall(red, "/api/v1/namespaces/default/configmaps/red"); + + CoreV1Api api = new CoreV1Api(); + + ConfigUtils.Prefix prefix = ConfigUtils.findPrefix("some", false, false, null); + NormalizedSource source = new NamedConfigMapNormalizedSource("application", NAMESPACE, true, prefix, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), true, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.application.default"); + Assertions.assertThat(sourceData.sourceData()).isEmpty(); + } + + /** + *
+	 *     - NamedSecretContextToSourceDataProvider gets as input a KubernetesClientConfigContext.
+	 *     - This context has a namespace as well as a NormalizedSource, that has a namespace too.
+	 *     - This test makes sure that we use the proper one.
+	 * 
+ */ + @Test + void namespaceMatch() { + + V1ConfigMap configMap = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME).withNamespace(NAMESPACE).build()) + .addToData(COLOR_REALLY_RED) + .build(); + stubCall(configMap, "/api/v1/namespaces/default/configmaps/red"); + + CoreV1Api api = new CoreV1Api(); + + String wrongNamespace = NAMESPACE + "nope"; + NormalizedSource source = new NamedConfigMapNormalizedSource(RED_CONFIG_MAP_NAME, wrongNamespace, true, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), true, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red.default"); + Assertions.assertThat(sourceData.sourceData()).isEqualTo(COLOR_REALLY_RED); + } + + /** + *
+	 *     - proves that single yaml file gets special treatment
+	 * 
+ */ + @Test + void testSingleYaml() { + V1ConfigMap singleYaml = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME).withNamespace(NAMESPACE).build()) + .addToData("single.yaml", "key: value") + .build(); + stubCall(singleYaml, "/api/v1/namespaces/default/configmaps/red"); + + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new NamedConfigMapNormalizedSource(RED_CONFIG_MAP_NAME, NAMESPACE, true, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), true, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red.default"); + Assertions.assertThat(sourceData.sourceData()).containsExactlyInAnyOrderEntriesOf(Map.of("key", "value")); + } + + /** + *
+	 *     - one configmap is deployed with name "one"
+	 *     - profile is enabled with name "k8s"
+	 *
+	 *     we assert that the name of the source is "one" and does not contain "one-dev"
+	 * 
+ */ + @Test + void testCorrectNameWithProfile() { + V1ConfigMap one = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("one").withNamespace(NAMESPACE).build()) + .addToData("key", "value") + .build(); + stubCall(one, "/api/v1/namespaces/default/configmaps/one"); + + CoreV1Api api = new CoreV1Api(); + + MockEnvironment environment = new MockEnvironment(); + environment.setActiveProfiles("k8s"); + + NormalizedSource source = new NamedConfigMapNormalizedSource("one", NAMESPACE, true, true); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + true, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.one.default"); + Assertions.assertThat(sourceData.sourceData()).containsExactlyInAnyOrderEntriesOf(Map.of("key", "value")); + } + + /** + *
+	 *     - one configmap is deployed with name "red"
+	 *     - one configmap is deployed with name "green"
+	 *
+	 *     - we first search for "red" and find it, and it is retrieved from the cluster via the client.
+	 * 	   - we then search for the "green" one, and it is retrieved again from the cluster, non cached.
+	 * 
+ */ + @Test + void nonCache(CapturedOutput output) { + V1ConfigMap red = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("red").withNamespace(NAMESPACE).build()) + .addToData("color", "red") + .build(); + stubCall(red, "/api/v1/namespaces/default/configmaps/red"); + + V1ConfigMap green = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("green").withNamespace(NAMESPACE).build()) + .addToData("color", "green") + .build(); + stubCall(green, "/api/v1/namespaces/default/configmaps/green"); + + CoreV1Api api = new CoreV1Api(); + + MockEnvironment environment = new MockEnvironment(); + + NormalizedSource redSource = new NamedConfigMapNormalizedSource("red", NAMESPACE, true, false); + KubernetesClientConfigContext redContext = new KubernetesClientConfigContext(api, redSource, NAMESPACE, + environment, true, NAMESPACED_BATCH_READ); + KubernetesClientContextToSourceData redData = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData redSourceData = redData.apply(redContext); + + Assertions.assertThat(redSourceData.sourceName()).isEqualTo("configmap.red.default"); + Assertions.assertThat(redSourceData.sourceData()).containsExactlyInAnyOrderEntriesOf(Map.of("color", "red")); + + Assertions.assertThat(output.getAll()) + .doesNotContain("Loaded all config maps in namespace '" + NAMESPACE + "'"); + Assertions.assertThat(output.getAll()).contains("Will read individual configmaps in namespace"); + + NormalizedSource greenSource = new NamedConfigMapNormalizedSource("green", NAMESPACE, true, true); + KubernetesClientConfigContext greenContext = new KubernetesClientConfigContext(api, greenSource, NAMESPACE, + environment, false, NAMESPACED_BATCH_READ); + KubernetesClientContextToSourceData greenData = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData greenSourceData = greenData.apply(greenContext); + + Assertions.assertThat(greenSourceData.sourceName()).isEqualTo("configmap.green.default"); + Assertions.assertThat(greenSourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("color", "green")); + + // meaning there is a single entry with such a log statement + String[] out = output.getAll().split("Loaded all config maps in namespace"); + Assertions.assertThat(out.length).isEqualTo(1); + + // meaning that the second read was done from the cache + out = output.getAll().split("Will read individual configmaps in namespace"); + Assertions.assertThat(out.length).isEqualTo(3); + + } + + private void stubCall(V1ConfigMap configMap, String path) { + stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(configMap)))); + } + +} 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/NamedSecretContextToSourceDataProviderNamespacedBatchReadTests.java similarity index 94% rename from spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/NamedSecretContextToSourceDataProviderTests.java rename to spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/NamedSecretContextToSourceDataProviderNamespacedBatchReadTests.java index af7ed9042a..5c9431bcdb 100644 --- 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/NamedSecretContextToSourceDataProviderNamespacedBatchReadTests.java @@ -31,6 +31,7 @@ import io.kubernetes.client.openapi.models.V1SecretListBuilder; import io.kubernetes.client.util.ClientBuilder; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -50,7 +51,9 @@ import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; @ExtendWith(OutputCaptureExtension.class) -class NamedSecretContextToSourceDataProviderTests { +class NamedSecretContextToSourceDataProviderNamespacedBatchReadTests { + + private static final boolean NAMESPACED_BATCH_READ = true; private static final ConfigUtils.Prefix PREFIX = ConfigUtils.findPrefix("some", false, false, "irrelevant"); @@ -73,7 +76,12 @@ static void setup() { @AfterEach void afterEach() { WireMock.reset(); - new KubernetesClientSecretsCache().discardAll(); + new KubernetesClientSourcesNamespaceBatched().discardSecrets(); + } + + @AfterAll + static void afterAll() { + WireMock.shutdownServer(); } /** @@ -93,7 +101,7 @@ void singleSecretMatchAgainstLabels() { // blue does not match red NormalizedSource source = new NamedSecretNormalizedSource("red", NAMESPACE, false, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -133,7 +141,7 @@ void twoSecretMatchAgainstLabels() { // blue does not match red, nor pink NormalizedSource source = new NamedSecretNormalizedSource("red", NAMESPACE, false, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -162,7 +170,7 @@ void testSecretNoMatch() { // blue does not match red NormalizedSource source = new NamedSecretNormalizedSource("blue", NAMESPACE, false, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -193,7 +201,7 @@ void namespaceMatch() { String wrongNamespace = NAMESPACE + "nope"; NormalizedSource source = new NamedSecretNormalizedSource("red", wrongNamespace, false, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -229,15 +237,14 @@ void matchIncludeSingleProfile() { MockEnvironment environment = new MockEnvironment(); environment.addActiveProfile("with-profile"); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, - true); + false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.red-with-profile.default.with-profile"); - Assertions.assertThat(sourceData.sourceData().size()).isEqualTo(2); + Assertions.assertThat(sourceData.sourceData().size()).isEqualTo(1); Assertions.assertThat(sourceData.sourceData().get("taste")).isEqualTo("mango"); - Assertions.assertThat(sourceData.sourceData().get("color")).isEqualTo("really-red"); } @@ -268,7 +275,8 @@ void matchIncludeSingleProfileWithPrefix() { NormalizedSource source = new NamedSecretNormalizedSource("red", NAMESPACE, true, PREFIX, true); MockEnvironment environment = new MockEnvironment(); environment.addActiveProfile("with-taste"); - KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + true, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -312,7 +320,8 @@ void matchIncludeTwoProfilesWithPrefix() { NormalizedSource source = new NamedSecretNormalizedSource("red", NAMESPACE, true, PREFIX, true); MockEnvironment environment = new MockEnvironment(); environment.setActiveProfiles("with-taste", "with-shape"); - KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + true, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -344,7 +353,7 @@ void testSingleYaml() { NormalizedSource source = new NamedSecretNormalizedSource("single-yaml", NAMESPACE, true, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -383,7 +392,7 @@ void cache(CapturedOutput output) { NormalizedSource redSource = new NamedSecretNormalizedSource("red", NAMESPACE, true, false); KubernetesClientConfigContext redContext = new KubernetesClientConfigContext(api, redSource, NAMESPACE, - environment); + environment, false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData redData = new NamedSecretContextToSourceDataProvider().get(); SourceData redSourceData = redData.apply(redContext); @@ -393,7 +402,7 @@ void cache(CapturedOutput output) { NormalizedSource greenSource = new NamedSecretNormalizedSource("green", NAMESPACE, true, true); KubernetesClientConfigContext greenContext = new KubernetesClientConfigContext(api, greenSource, NAMESPACE, - environment); + environment, false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData greenData = new NamedSecretContextToSourceDataProvider().get(); SourceData greenSourceData = greenData.apply(greenContext); diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/NamedSecretContextToSourceDataProviderNonNamespacedBatchReadTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/NamedSecretContextToSourceDataProviderNonNamespacedBatchReadTests.java new file mode 100644 index 0000000000..3173889179 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/NamedSecretContextToSourceDataProviderNonNamespacedBatchReadTests.java @@ -0,0 +1,422 @@ +/* + * Copyright 2013-2024 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.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.V1Secret; +import io.kubernetes.client.openapi.models.V1SecretBuilder; +import io.kubernetes.client.util.ClientBuilder; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.kubernetes.commons.config.ConfigUtils; +import org.springframework.cloud.kubernetes.commons.config.NamedSecretNormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +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; + +@ExtendWith(OutputCaptureExtension.class) +class NamedSecretContextToSourceDataProviderNonNamespacedBatchReadTests { + + private static final boolean NAMESPACED_BATCH_READ = false; + + private static final ConfigUtils.Prefix PREFIX = ConfigUtils.findPrefix("some", false, false, "irrelevant"); + + private static final String NAMESPACE = "default"; + + private static final Map COLOR_REALLY_RED = Map.of("color", "really-red".getBytes()); + + @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(); + new KubernetesClientSourcesNamespaceBatched().discardSecrets(); + } + + @AfterAll + static void afterAll() { + WireMock.shutdownServer(); + } + + /** + * we have a single secret deployed. it matched the name in our queries + */ + @Test + void singleSecretMatchAgainstLabels() { + + V1Secret red = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("red").build()) + .addToData(COLOR_REALLY_RED) + .build(); + stubCall(red, "/api/v1/namespaces/default/secrets/red"); + CoreV1Api api = new CoreV1Api(); + + // blue does not match red + NormalizedSource source = new NamedSecretNormalizedSource("red", NAMESPACE, false, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(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() { + + V1Secret red = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("red").build()) + .addToData(COLOR_REALLY_RED) + .build(); + stubCall(red, "/api/v1/namespaces/default/secrets/red"); + + V1Secret blue = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("blue").build()) + .addToData(COLOR_REALLY_RED) + .build(); + stubCall(blue, "/api/v1/namespaces/default/secrets/blue"); + + V1Secret pink = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("pink").build()) + .addToData(COLOR_REALLY_RED) + .build(); + stubCall(pink, "/api/v1/namespaces/default/secrets/pink"); + + CoreV1Api api = new CoreV1Api(); + + // blue does not match red, nor pink + NormalizedSource source = new NamedSecretNormalizedSource("red", NAMESPACE, false, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.default"); + Assertions.assertThat(sourceData.sourceData()).hasSize(1); + Assertions.assertThat(sourceData.sourceData().get("color")).isEqualTo("really-red"); + + } + + /** + * one secret deployed (pink), does not match our query (blue). + */ + @Test + void testSecretNoMatch() { + + V1Secret secret = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("red").build()) + .addToData(COLOR_REALLY_RED) + .build(); + + stubCall(secret, "/api/v1/namespaces/default/secrets/blue"); + CoreV1Api api = new CoreV1Api(); + + // blue does not match red + NormalizedSource source = new NamedSecretNormalizedSource("blue", NAMESPACE, false, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.blue.default"); + Assertions.assertThat(sourceData.sourceData()).isEmpty(); + } + + /** + *
+	 *     - LabeledSecretContextToSourceDataProvider gets as input a KubernetesClientConfigContext.
+	 *     - This context has a namespace as well as a NormalizedSource, that has a namespace too.
+	 *     - This test makes sure that we use the proper one.
+	 * 
+ */ + @Test + void namespaceMatch() { + + V1Secret secret = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("red").build()) + .addToData(COLOR_REALLY_RED) + .build(); + + stubCall(secret, "/api/v1/namespaces/default/secrets/red"); + CoreV1Api api = new CoreV1Api(); + + String wrongNamespace = NAMESPACE + "nope"; + NormalizedSource source = new NamedSecretNormalizedSource("red", wrongNamespace, false, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("color", "really-red")); + } + + /** + * we have two secrets deployed. one matches the query name. the other matches the + * active profile + name, thus is taken also. + */ + @Test + void matchIncludeSingleProfile() { + + V1Secret red = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("red").build()) + .addToData(COLOR_REALLY_RED) + .build(); + stubCall(red, "/api/v1/namespaces/default/secrets/red"); + + V1Secret mango = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("red-with-profile").build()) + .addToData("taste", "mango".getBytes()) + .build(); + stubCall(mango, "/api/v1/namespaces/default/secrets/red-with-profile"); + + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new NamedSecretNormalizedSource("red", NAMESPACE, false, ConfigUtils.Prefix.DEFAULT, + true, true); + MockEnvironment environment = new MockEnvironment(); + environment.addActiveProfile("with-profile"); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.red-with-profile.default.with-profile"); + Assertions.assertThat(sourceData.sourceData()).hasSize(1); + Assertions.assertThat(sourceData.sourceData().get("taste")).isEqualTo("mango"); + + } + + /** + * we have two secrets 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 + * secrets as well. + */ + @Test + void matchIncludeSingleProfileWithPrefix() { + + V1Secret red = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("red").build()) + .addToData(COLOR_REALLY_RED) + .build(); + stubCall(red, "/api/v1/namespaces/default/secrets/red"); + + V1Secret mango = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("red-with-taste").build()) + .addToData("taste", "mango".getBytes()) + .build(); + stubCall(mango, "/api/v1/namespaces/default/secrets/red-with-taste"); + + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new NamedSecretNormalizedSource("red", NAMESPACE, true, PREFIX, true); + MockEnvironment environment = new MockEnvironment(); + environment.addActiveProfile("with-taste"); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + true, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.red-with-taste.default"); + Assertions.assertThat(sourceData.sourceData()).hasSize(2); + Assertions.assertThat(sourceData.sourceData().get("some.color")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData().get("some.taste")).isEqualTo("mango"); + + } + + /** + * we have three secrets 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() { + + V1Secret red = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("red").build()) + .addToData(COLOR_REALLY_RED) + .build(); + stubCall(red, "/api/v1/namespaces/default/secrets/red"); + + V1Secret mango = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("red-with-taste").build()) + .addToData("taste", "mango".getBytes()) + .build(); + stubCall(mango, "/api/v1/namespaces/default/secrets/red-with-taste"); + + V1Secret shape = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("red-with-shape").build()) + .addToData("shape", "round".getBytes()) + .build(); + stubCall(shape, "/api/v1/namespaces/default/secrets/red-with-shape"); + + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new NamedSecretNormalizedSource("red", NAMESPACE, true, PREFIX, true); + MockEnvironment environment = new MockEnvironment(); + environment.setActiveProfiles("with-taste", "with-shape"); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + true, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.red-with-shape.red-with-taste.default"); + + Assertions.assertThat(sourceData.sourceData()).hasSize(3); + Assertions.assertThat(sourceData.sourceData().get("some.color")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData().get("some.taste")).isEqualTo("mango"); + Assertions.assertThat(sourceData.sourceData().get("some.shape")).isEqualTo("round"); + + } + + /** + *
+	 *     - proves that single yaml file gets special treatment
+	 * 
+ */ + @Test + void testSingleYaml() { + V1Secret singleYaml = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("single-yaml").withNamespace(NAMESPACE).build()) + .addToData("single.yaml", "key: value".getBytes()) + .build(); + stubCall(singleYaml, "/api/v1/namespaces/default/secrets/single-yaml"); + + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new NamedSecretNormalizedSource("single-yaml", NAMESPACE, true, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, NAMESPACED_BATCH_READ); + + KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.single-yaml.default"); + Assertions.assertThat(sourceData.sourceData()).containsExactlyInAnyOrderEntriesOf(Map.of("key", "value")); + } + + /** + *
+	 *     - one secret is deployed with name "red"
+	 *     - one secret is deployed with name "green"
+	 *
+	 *     - we first search for "red" and find it, and it is retrieved from the cluster via the client.
+	 * 	   - we then search for the "green" one, and it is retrieved again from the cluster, non cached.
+	 * 
+ */ + @Test + void nonCache(CapturedOutput output) { + V1Secret red = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("red").withNamespace(NAMESPACE).build()) + .addToData("color", "red".getBytes()) + .build(); + stubCall(red, "/api/v1/namespaces/default/secrets/red"); + + V1Secret green = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("green").withNamespace(NAMESPACE).build()) + .addToData("color", "green".getBytes()) + .build(); + stubCall(green, "/api/v1/namespaces/default/secrets/green"); + + CoreV1Api api = new CoreV1Api(); + + MockEnvironment environment = new MockEnvironment(); + + NormalizedSource redSource = new NamedSecretNormalizedSource("red", NAMESPACE, true, false); + KubernetesClientConfigContext redContext = new KubernetesClientConfigContext(api, redSource, NAMESPACE, + environment, false, NAMESPACED_BATCH_READ); + KubernetesClientContextToSourceData redData = new NamedSecretContextToSourceDataProvider().get(); + SourceData redSourceData = redData.apply(redContext); + + Assertions.assertThat(redSourceData.sourceName()).isEqualTo("secret.red.default"); + Assertions.assertThat(redSourceData.sourceData()).containsExactlyInAnyOrderEntriesOf(Map.of("color", "red")); + + Assertions.assertThat(output.getAll()).doesNotContain(("Loaded all secrets in namespace '" + NAMESPACE + "'")); + Assertions.assertThat(output.getAll()).contains("Will read individual secrets in namespace"); + + NormalizedSource greenSource = new NamedSecretNormalizedSource("green", NAMESPACE, true, true); + KubernetesClientConfigContext greenContext = new KubernetesClientConfigContext(api, greenSource, NAMESPACE, + environment, false, NAMESPACED_BATCH_READ); + KubernetesClientContextToSourceData greenData = new NamedSecretContextToSourceDataProvider().get(); + SourceData greenSourceData = greenData.apply(greenContext); + + Assertions.assertThat(greenSourceData.sourceName()).isEqualTo("secret.green.default"); + Assertions.assertThat(greenSourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("color", "green")); + + // meaning there is a single entry with such a log statement + String[] out = output.getAll().split("Loaded all secrets in namespace"); + Assertions.assertThat(out.length).isEqualTo(1); + + // meaning that the second read was done from the cache + out = output.getAll().split("Will read individual secrets in namespace"); + Assertions.assertThat(out.length).isEqualTo(3); + + } + + private void stubCall(V1Secret secret, String path) { + stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(secret)))); + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_secret_with_profile/LabeledSecretWithProfileTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_secret_with_profile/LabeledSecretWithProfileTests.java index 941dfb97d6..02ed60822d 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_secret_with_profile/LabeledSecretWithProfileTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_secret_with_profile/LabeledSecretWithProfileTests.java @@ -81,8 +81,7 @@ static void afterAll() { /** *
-	 *     this one is taken from : "blue.one". We find "color-secret" by labels, and
-	 *     "color-secrets-k8s" exists, but "includeProfileSpecificSources=false", thus not taken.
+	 *     this one is taken from : "blue.one". We find "color-secret" by labels.
 	 *     Since "explicitPrefix=blue", we take "blue.one"
 	 * 
*/ diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/sources_order/SourcesOrderTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/sources_order/SourcesOrderTests.java index 3f43f594ee..5798872aa9 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/sources_order/SourcesOrderTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/sources_order/SourcesOrderTests.java @@ -16,10 +16,15 @@ package org.springframework.cloud.kubernetes.client.config.applications.sources_order; +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.util.ClientBuilder; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -29,6 +34,8 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.reactive.server.WebTestClient; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; + /** * The stub data for this test is in : * {@link org.springframework.cloud.kubernetes.client.config.bootstrap.stubs.SourcesOrderConfigurationStub} @@ -45,6 +52,18 @@ abstract class SourcesOrderTests { @Autowired private WebTestClient webClient; + @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(); diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/LabeledConfigMapWithProfileConfigurationStub.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/LabeledConfigMapWithProfileConfigurationStub.java index 8e1ed61203..3133bd27e5 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/LabeledConfigMapWithProfileConfigurationStub.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/LabeledConfigMapWithProfileConfigurationStub.java @@ -73,9 +73,6 @@ public ApiClient apiClient(WireMockServer wireMockServer) { * - configmap with name "color-configmap-k8s", with labels : "{color: not-blue}" * - configmap with name "green-configmap-k8s", with labels : "{color: green-k8s}" * - configmap with name "green-configmap-prod", with labels : "{color: green-prod}" - * - * # a test that proves order: first read non-profile based configmaps, thus profile based - * # configmaps override non-profile ones. * - configmap with name "green-purple-configmap", labels "{color: green, shape: round}", data: "{eight: 8}" * - configmap with name "green-purple-configmap-k8s", labels "{color: black}", data: "{eight: eight-ish}" * @@ -112,7 +109,7 @@ public static void stubData() { V1ConfigMap greenConfigMapK8s = new V1ConfigMapBuilder() .withMetadata(new V1ObjectMetaBuilder().withName("green-configmap-k8s") .withNamespace("spring-k8s") - .withLabels(Map.of("color", "green-k8s")) + .withLabels(Map.of("color", "green")) .build()) .addToData(Collections.singletonMap("six", "6")) .build(); @@ -121,7 +118,7 @@ public static void stubData() { V1ConfigMap greenConfigMapProd = new V1ConfigMapBuilder() .withMetadata(new V1ObjectMetaBuilder().withName("green-configmap-prod") .withNamespace("spring-k8s") - .withLabels(Map.of("color", "green-prod")) + .withLabels(Map.of("color", "green")) .build()) .addToData(Collections.singletonMap("seven", "7")) .build(); @@ -157,7 +154,7 @@ public static void stubData() { V1ConfigMap greenPurpleConfigMapK8s = new V1ConfigMapBuilder() .withMetadata(new V1ObjectMetaBuilder().withName("green-purple-configmap-k8s") .withNamespace("spring-k8s") - .withLabels(Map.of("color", "black")) + .withLabels(Map.of("color", "green")) .build()) .addToData(Collections.singletonMap("eight", "eight-ish")) .build(); diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/LabeledSecretWithProfileConfigurationStub.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/LabeledSecretWithProfileConfigurationStub.java index c59ca39f1c..072bc291f8 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/LabeledSecretWithProfileConfigurationStub.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/bootstrap/stubs/LabeledSecretWithProfileConfigurationStub.java @@ -74,9 +74,6 @@ public ApiClient apiClient(WireMockServer wireMockServer) { * - secret with name "color-secret-k8s", with labels : "{color: not-blue}" * - secret with name "green-secret-k8s", with labels : "{color: green-k8s}" * - secret with name "green-secret-prod", with labels : "{color: green-prod}" - * - * # a test that proves order: first read non-profile based secrets, thus profile based - * # secrets override non-profile ones. * - secret with name "green-purple-secret", labels "{color: green, shape: round}", data: "{eight: 8}" * - secret with name "green-purple-secret-k8s", labels "{color: black}", data: "{eight: eight-ish}" * @@ -113,7 +110,7 @@ public static void stubData() { V1Secret greenSecretK8s = new V1SecretBuilder() .withMetadata(new V1ObjectMetaBuilder().withName("green-secret-k8s") .withNamespace("spring-k8s") - .withLabels(Map.of("color", "green-k8s")) + .withLabels(Map.of("color", "green")) .build()) .addToData(Collections.singletonMap("six", "6".getBytes(StandardCharsets.UTF_8))) .build(); @@ -122,7 +119,7 @@ public static void stubData() { V1Secret shapeSecretProd = new V1SecretBuilder() .withMetadata(new V1ObjectMetaBuilder().withName("green-secret-prod") .withNamespace("spring-k8s") - .withLabels(Map.of("color", "green-prod")) + .withLabels(Map.of("color", "green")) .build()) .addToData(Collections.singletonMap("seven", "7".getBytes(StandardCharsets.UTF_8))) .build(); @@ -158,7 +155,7 @@ public static void stubData() { V1Secret greenPurpleSecretK8s = new V1SecretBuilder() .withMetadata(new V1ObjectMetaBuilder().withName("green-purple-secret-k8s") .withNamespace("spring-k8s") - .withLabels(Map.of("color", "black")) + .withLabels(Map.of("color", "green")) .build()) .addToData(Collections.singletonMap("eight", "eight-ish".getBytes(StandardCharsets.UTF_8))) .build(); diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadConfigMapTest.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadConfigMapTest.java index 7d30a2a6c9..1bf718c64c 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadConfigMapTest.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadConfigMapTest.java @@ -217,7 +217,7 @@ AbstractEnvironment environment() { // otherwise we can't properly test reload functionality ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, false, true, FAIL_FAST, - RetryProperties.DEFAULT); + RetryProperties.DEFAULT, true); KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); PropertySource propertySource = new KubernetesClientConfigMapPropertySourceLocator(coreV1Api, @@ -240,7 +240,7 @@ ConfigReloadProperties configReloadProperties() { @Primary ConfigMapConfigProperties configMapConfigProperties() { return new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, - false, true, FAIL_FAST, RetryProperties.DEFAULT); + false, true, FAIL_FAST, RetryProperties.DEFAULT, true); } @Bean diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadSecretTest.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadSecretTest.java index 5cc87004b0..9864c8e29d 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadSecretTest.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadSecretTest.java @@ -221,7 +221,7 @@ AbstractEnvironment environment() { // KubernetesClientConfigMapPropertySource, // otherwise we can't properly test reload functionality SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), - List.of(), true, SECRET_NAME, NAMESPACE, false, true, FAIL_FAST, RetryProperties.DEFAULT); + List.of(), true, SECRET_NAME, NAMESPACE, false, true, FAIL_FAST, RetryProperties.DEFAULT, true); KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); PropertySource propertySource = new KubernetesClientSecretsPropertySourceLocator(coreV1Api, @@ -244,7 +244,7 @@ ConfigReloadProperties configReloadProperties() { @Primary SecretsConfigProperties secretsConfigProperties() { return new SecretsConfigProperties(true, Map.of(), List.of(), List.of(), true, SECRET_NAME, NAMESPACE, - false, true, FAIL_FAST, RetryProperties.DEFAULT); + false, true, FAIL_FAST, RetryProperties.DEFAULT, true); } @Bean diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadConfigMapTest.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadConfigMapTest.java index 53467860c5..c9e38c56ef 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadConfigMapTest.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadConfigMapTest.java @@ -192,7 +192,8 @@ AbstractEnvironment environment() { // KubernetesClientConfigMapPropertySource, // otherwise we can't properly test reload functionality ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), - List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT); + List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT, + true); KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); PropertySource propertySource = new KubernetesClientConfigMapPropertySourceLocator(coreV1Api, @@ -215,7 +216,7 @@ ConfigReloadProperties configReloadProperties() { @Primary ConfigMapConfigProperties configMapConfigProperties() { return new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, - false, true, FAIL_FAST, RetryProperties.DEFAULT); + false, true, FAIL_FAST, RetryProperties.DEFAULT, true); } @Bean diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadSecretTest.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadSecretTest.java index 8a30192717..68ba433529 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadSecretTest.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadSecretTest.java @@ -194,7 +194,7 @@ AbstractEnvironment environment() { // KubernetesClientSecretPropertySource, // otherwise we can't properly test reload functionality SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), - List.of(), true, SECRET_NAME, NAMESPACE, false, true, false, RetryProperties.DEFAULT); + List.of(), true, SECRET_NAME, NAMESPACE, false, true, false, RetryProperties.DEFAULT, true); KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); PropertySource propertySource = new KubernetesClientSecretsPropertySourceLocator(coreV1Api, @@ -217,7 +217,7 @@ ConfigReloadProperties configReloadProperties() { @Primary SecretsConfigProperties secretsConfigProperties() { return new SecretsConfigProperties(true, Map.of(), List.of(), List.of(), true, SECRET_NAME, NAMESPACE, - false, true, FAIL_FAST, RetryProperties.DEFAULT); + false, true, FAIL_FAST, RetryProperties.DEFAULT, true); } @Bean diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigDataRetryableConfigMapPropertySourceLocator.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigDataRetryableConfigMapPropertySourceLocator.java index 6bb155bca9..90a7afe585 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigDataRetryableConfigMapPropertySourceLocator.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigDataRetryableConfigMapPropertySourceLocator.java @@ -65,8 +65,8 @@ public ConfigDataRetryableConfigMapPropertySourceLocator( @Override protected MapPropertySource getMapPropertySource(NormalizedSource normalizedSource, - ConfigurableEnvironment environment) { - return configMapPropertySourceLocator.getMapPropertySource(normalizedSource, environment); + ConfigurableEnvironment environment, boolean namespacedBatchRead) { + return configMapPropertySourceLocator.getMapPropertySource(normalizedSource, environment, namespacedBatchRead); } @Override diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigDataRetryableSecretsPropertySourceLocator.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigDataRetryableSecretsPropertySourceLocator.java index d0c70e72f3..2e14df8a34 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigDataRetryableSecretsPropertySourceLocator.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigDataRetryableSecretsPropertySourceLocator.java @@ -73,8 +73,8 @@ public Collection> locateCollection(Environment environment) { @Override protected SecretsPropertySource getPropertySource(ConfigurableEnvironment environment, - NormalizedSource normalizedSource) { - return this.secretsPropertySourceLocator.getPropertySource(environment, normalizedSource); + NormalizedSource normalizedSource, boolean namespacedBatchRead) { + return this.secretsPropertySourceLocator.getPropertySource(environment, normalizedSource, namespacedBatchRead); } public SecretsPropertySourceLocator getSecretsPropertySourceLocator() { diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapCache.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapCache.java index 8c55724660..5ffae1b338 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapCache.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapCache.java @@ -24,7 +24,7 @@ public interface ConfigMapCache { /** * Discards all stored entries from the cache. */ - void discardAll(); + void discardConfigMaps(); /** * an implementation that does nothing. In the next major release it will become @@ -33,7 +33,7 @@ public interface ConfigMapCache { class NOOPCache implements ConfigMapCache { @Override - public void discardAll() { + public void discardConfigMaps() { } } 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 0fb4f504cc..4241707068 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 @@ -39,7 +39,7 @@ public record ConfigMapConfigProperties(@DefaultValue("true") boolean enableApi, @DefaultValue List sources, @DefaultValue Map labels, @DefaultValue("true") boolean enabled, String name, String namespace, boolean useNameAsPrefix, @DefaultValue("true") boolean includeProfileSpecificSources, boolean failFast, - @DefaultValue RetryProperties retry) { + @DefaultValue RetryProperties retry, @DefaultValue("true") boolean namespacedBatchRead) { /** * Prefix for Kubernetes config maps configuration properties. 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 4fe25ff676..f17432be74 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 @@ -72,7 +72,7 @@ public ConfigMapPropertySourceLocator(ConfigMapConfigProperties properties, Conf } protected abstract MapPropertySource getMapPropertySource(NormalizedSource normalizedSource, - ConfigurableEnvironment environment); + ConfigurableEnvironment environment, boolean namespacedBatchRead); @Override public PropertySource locate(Environment environment) { @@ -83,7 +83,7 @@ public PropertySource locate(Environment environment) { Set sources = new LinkedHashSet<>(this.properties.determineSources(environment)); LOG.debug("Config Map normalized sources : " + sources); sources.forEach(s -> { - MapPropertySource propertySource = getMapPropertySource(s, env); + MapPropertySource propertySource = getMapPropertySource(s, env, properties.namespacedBatchRead()); if ("true".equals(propertySource.getProperty(Constants.ERROR_PROPERTY))) { LOG.warn("Failed to load source: " + s); } @@ -96,7 +96,7 @@ public PropertySource locate(Environment environment) { addPropertySourcesFromPaths(environment, composite); - cache.discardAll(); + cache.discardConfigMaps(); return composite; } return null; 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 05fd1f166c..8e50e6c787 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 @@ -16,7 +16,6 @@ package org.springframework.cloud.kubernetes.commons.config; -import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.HashMap; @@ -24,7 +23,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.function.BiPredicate; import java.util.function.BooleanSupplier; import java.util.function.Function; @@ -157,21 +155,6 @@ public static void onException(boolean failFast, Exception e) { LOG.warn(e.getMessage() + ". Ignoring.", e); } - /* - * 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" - */ - @Deprecated(forRemoval = true) - public static SourceData withPrefix(String target, PrefixContext 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().stream().sorted().collect(Collectors.toCollection(LinkedHashSet::new))); - return new SourceData(sourceName(target, propertySourceTokens, context.namespace()), withPrefix); - } - public static String sourceName(String target, String applicationName, String namespace) { return target + PROPERTY_SOURCE_NAME_SEPARATOR + applicationName + PROPERTY_SOURCE_NAME_SEPARATOR + namespace; } @@ -186,6 +169,9 @@ public static String sourceName(String target, String applicationName, String na public static MultipleSourcesContainer processNamedData(List strippedSources, Environment environment, LinkedHashSet sourceNames, String namespace, boolean decode) { + if (strippedSources.isEmpty()) { + return MultipleSourcesContainer.empty(); + } return processNamedData(strippedSources, environment, sourceNames, namespace, decode, true); } @@ -198,6 +184,10 @@ public static MultipleSourcesContainer processNamedData(List sourceNames, String namespace, boolean decode, boolean includeDefaultProfileData) { + if (strippedSources.isEmpty()) { + return MultipleSourcesContainer.empty(); + } + Map hashByName = strippedSources.stream() .collect(Collectors.toMap(StrippedSourceContainer::name, Function.identity())); @@ -220,11 +210,14 @@ public static MultipleSourcesContainer processNamedData(List>>>>>> fix-1715-drop-profiles-support false then we want to make sure + * that we only return properties from any active profiles */ if (processSource(includeDefaultProfileData, environment, sourceName, rawData)) { Map processedData = SourceDataEntriesProcessor.processAllEntries( @@ -281,47 +274,26 @@ static String sourceDataName(String target, LinkedHashSet sourceNames, S /** * Transforms raw data from one or multiple sources into an entry of source names and * flattened data that they all hold (potentially overriding entries without any - * defined order). This method first searches by labels, find the sources, then uses - * these names to find any profile-based sources. + * defined order). */ - public static MultipleSourcesContainer processLabeledData(List containers, - Environment environment, Map labels, String namespace, Set profiles, - boolean decode) { + public static MultipleSourcesContainer processLabeledData(List strippedSources, + Environment environment, Map labels, String namespace, boolean decode) { + + if (strippedSources.isEmpty()) { + return MultipleSourcesContainer.empty(); + } // find sources by provided labels - List byLabels = containers.stream().filter(one -> { + List byLabels = strippedSources.stream().filter(one -> { Map sourceLabels = one.labels(); Map labelsToSearchAgainst = sourceLabels == null ? Map.of() : sourceLabels; return labelsToSearchAgainst.entrySet().containsAll((labels.entrySet())); }).toList(); - // Compute profile-based source names (based on the ones we found by labels) - List sourceNamesByLabelsWithProfile = new ArrayList<>(); - if (profiles != null && !profiles.isEmpty()) { - for (StrippedSourceContainer one : byLabels) { - for (String profile : profiles) { - String name = one.name() + "-" + profile; - sourceNamesByLabelsWithProfile.add(name); - } - } - } - - // Once we know sources by labels (and thus their names), we can find out - // profiles based sources from the above. This would get all sources - // we are interested in. - List byProfile = containers.stream() - .filter(one -> sourceNamesByLabelsWithProfile.contains(one.name())) - .toList(); - - // this makes sure that we first have "app" and then "app-dev" in the list - List all = new ArrayList<>(byLabels.size() + byProfile.size()); - all.addAll(byLabels); - all.addAll(byProfile); - LinkedHashSet sourceNames = new LinkedHashSet<>(); Map data = new HashMap<>(); - all.forEach(source -> { + byLabels.forEach(source -> { String foundSourceName = source.name(); LOG.debug("Loaded source with name : '" + foundSourceName + " in namespace: '" + namespace + "'"); sourceNames.add(foundSourceName); 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 index aecd0f35fe..4f32097c0e 100644 --- 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 @@ -31,22 +31,17 @@ public final class LabeledSecretNormalizedSource extends NormalizedSource { private final ConfigUtils.Prefix prefix; - private final boolean includeProfileSpecificSources; - public LabeledSecretNormalizedSource(String namespace, Map labels, boolean failFast, - ConfigUtils.Prefix prefix, boolean includeProfileSpecificSources) { + ConfigUtils.Prefix prefix) { super(null, namespace, failFast); this.labels = Collections.unmodifiableMap(Objects.requireNonNull(labels)); this.prefix = Objects.requireNonNull(prefix); - this.includeProfileSpecificSources = includeProfileSpecificSources; } - public LabeledSecretNormalizedSource(String namespace, Map labels, boolean failFast, - boolean includeProfileSpecificSources) { + public LabeledSecretNormalizedSource(String namespace, Map labels, boolean failFast) { super(null, namespace, failFast); this.labels = Collections.unmodifiableMap(Objects.requireNonNull(labels)); this.prefix = ConfigUtils.Prefix.DEFAULT; - this.includeProfileSpecificSources = includeProfileSpecificSources; } /** @@ -60,10 +55,6 @@ public ConfigUtils.Prefix prefix() { return prefix; } - public boolean profileSpecificSources() { - return this.includeProfileSpecificSources; - } - @Override public NormalizedSourceType type() { return NormalizedSourceType.LABELED_SECRET; diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/LabeledSourceData.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/LabeledSourceData.java index 5a18a096dc..d402135e62 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/LabeledSourceData.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/LabeledSourceData.java @@ -16,10 +16,8 @@ package org.springframework.cloud.kubernetes.commons.config; -import java.util.Arrays; import java.util.LinkedHashSet; import java.util.Map; -import java.util.Set; import java.util.stream.Collectors; import org.apache.commons.logging.Log; @@ -45,18 +43,14 @@ public abstract class LabeledSourceData { private static final Log LOG = LogFactory.getLog(LabeledSourceData.class); - public final SourceData compute(Map labels, Prefix prefix, String target, boolean profileSources, - boolean failFast, String namespace, String[] activeProfiles) { + public final SourceData compute(Map labels, ConfigUtils.Prefix prefix, String target, + boolean failFast, String namespace) { MultipleSourcesContainer data = MultipleSourcesContainer.empty(); String sourceDataName; try { - Set profiles = Set.of(); - if (profileSources) { - profiles = Arrays.stream(activeProfiles).collect(Collectors.toSet()); - } - data = dataSupplier(labels, profiles); + data = dataSupplier(labels); LinkedHashSet sourceNames = data.names(); Map sourceDataForSourceName = data.data(); @@ -107,11 +101,9 @@ private SourceData emptySourceData(Map labels, String target, St * Implementation specific (fabric8 or k8s-native) way to get the data from then given * source names. * @param labels the ones that have been configured - * @param profiles profiles to taken into account when gathering source data. Can be - * empty. * @return a container that holds the names of the source that were found and their * data */ - public abstract MultipleSourcesContainer dataSupplier(Map labels, Set profiles); + public abstract MultipleSourcesContainer dataSupplier(Map labels); } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/NamedSourceData.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/NamedSourceData.java index f87d078336..999a8e6284 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/NamedSourceData.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/NamedSourceData.java @@ -106,7 +106,7 @@ private SourceData emptySourceData(String target, String sourceName, String name return SourceData.emptyRecord(emptySourceName); } - protected String generateSourceName(String target, String sourceName, String namespace, String[] activeProfiles) { + protected String generateSourceName(String target, String sourceName, String namespace, String[] activeProfile) { return ConfigUtils.sourceName(target, sourceName, namespace); } @@ -117,6 +117,6 @@ protected String generateSourceName(String target, String sourceName, String nam * preserve the order: non-profile source first and then the rest * @return an Entry that holds the names of the source that were found and their data */ - public abstract MultipleSourcesContainer dataSupplier(LinkedHashSet sourceNames); + protected abstract MultipleSourcesContainer dataSupplier(LinkedHashSet sourceNames); } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/PrefixContext.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/PrefixContext.java deleted file mode 100644 index 720998e34a..0000000000 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/PrefixContext.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 secret or - * config map. - * - * @author wind57 - * @deprecated will be deleted in a future release. - */ -@Deprecated(forRemoval = true) -public record PrefixContext(Map data, String prefix, String namespace, - Set propertySourceNames) { -} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsCache.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsCache.java index 90a35bc264..3c8500c061 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsCache.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsCache.java @@ -24,7 +24,7 @@ public interface SecretsCache { /** * Discards all stored entries from the cache. */ - void discardAll(); + void discardSecrets(); /** * an implementation that does nothing. In the next major release it will become @@ -33,7 +33,7 @@ public interface SecretsCache { class NOOPCache implements SecretsCache { @Override - public void discardAll() { + public void discardSecrets() { } } 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 c8ddda3def..b1d09bd9f8 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 @@ -40,7 +40,7 @@ public record SecretsConfigProperties(boolean enableApi, @DefaultValue Map paths, @DefaultValue List sources, @DefaultValue("true") boolean enabled, String name, String namespace, boolean useNameAsPrefix, @DefaultValue("true") boolean includeProfileSpecificSources, boolean failFast, - @DefaultValue RetryProperties retry) { + @DefaultValue RetryProperties retry, @DefaultValue("true") boolean namespacedBatchRead) { /** * Prefix for Kubernetes secrets configuration properties. @@ -59,7 +59,7 @@ public List determineSources(Environment environment) { if (!labels.isEmpty()) { result.add(new LabeledSecretNormalizedSource(this.namespace, this.labels, this.failFast, - ConfigUtils.Prefix.DEFAULT, false)); + ConfigUtils.Prefix.DEFAULT)); } return result; } @@ -105,7 +105,7 @@ private Stream normalize(String defaultName, String defaultNam if (!normalizedLabels.isEmpty()) { NormalizedSource labeledBasedSource = new LabeledSecretNormalizedSource(normalizedNamespace, labels, - failFast, prefix, includeProfileSpecificSources); + failFast, prefix); normalizedSources.add(labeledBasedSource); } 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 cecd3232ea..f32c6dfe72 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 @@ -90,7 +90,8 @@ public PropertySource locate(Environment environment) { if (this.properties.enableApi()) { uniqueSources.forEach(s -> { - MapPropertySource propertySource = getSecretsPropertySourceForSingleSecret(env, s); + MapPropertySource propertySource = getSecretsPropertySourceForSingleSecret(env, s, + properties.namespacedBatchRead()); if ("true".equals(propertySource.getProperty(Constants.ERROR_PROPERTY))) { LOG.warn("Failed to load source: " + s); @@ -102,7 +103,7 @@ public PropertySource locate(Environment environment) { }); } - cache.discardAll(); + cache.discardSecrets(); return composite; } return null; @@ -114,13 +115,13 @@ public Collection> locateCollection(Environment environment) { } private SecretsPropertySource getSecretsPropertySourceForSingleSecret(ConfigurableEnvironment environment, - NormalizedSource normalizedSource) { + NormalizedSource normalizedSource, boolean namespacedBatchRead) { - return getPropertySource(environment, normalizedSource); + return getPropertySource(environment, normalizedSource, namespacedBatchRead); } protected abstract SecretsPropertySource getPropertySource(ConfigurableEnvironment environment, - NormalizedSource normalizedSource); + NormalizedSource normalizedSource, boolean namespacedBatchRead); protected void putPathConfig(CompositePropertySource composite) { diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigPropertiesBindingTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigPropertiesBindingTests.java index e6cb207587..8ae0ed472f 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigPropertiesBindingTests.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigPropertiesBindingTests.java @@ -46,6 +46,7 @@ void testWithDefaults() { Assertions.assertThat(props.useNameAsPrefix()).isFalse(); Assertions.assertThat(props.includeProfileSpecificSources()).isTrue(); Assertions.assertThat(props.failFast()).isFalse(); + Assertions.assertThat(props.namespacedBatchRead()).isTrue(); Assertions.assertThat(props.retry()).isNotNull(); Assertions.assertThat(props.retry().initialInterval()).isEqualTo(1000L); @@ -53,6 +54,7 @@ void testWithDefaults() { Assertions.assertThat(props.retry().maxInterval()).isEqualTo(2000L); Assertions.assertThat(props.retry().maxAttempts()).isEqualTo(6); Assertions.assertThat(props.retry().enabled()).isTrue(); + }); } @@ -77,7 +79,8 @@ void testWithNonDefaults() { "spring.cloud.kubernetes.config.retry.multiplier=1.2", "spring.cloud.kubernetes.config.retry.max-interval=3", "spring.cloud.kubernetes.config.retry.max-attempts=4", - "spring.cloud.kubernetes.config.retry.enabled=false") + "spring.cloud.kubernetes.config.retry.enabled=false", + "spring.cloud.kubernetes.config.namespaced-batch-read=false") .run(context -> { ConfigMapConfigProperties props = context.getBean(ConfigMapConfigProperties.class); Assertions.assertThat(props).isNotNull(); @@ -106,6 +109,7 @@ void testWithNonDefaults() { Assertions.assertThat(props.useNameAsPrefix()).isTrue(); Assertions.assertThat(props.includeProfileSpecificSources()).isTrue(); Assertions.assertThat(props.failFast()).isTrue(); + Assertions.assertThat(props.namespacedBatchRead()).isFalse(); RetryProperties retryProperties = props.retry(); Assertions.assertThat(retryProperties).isNotNull(); 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 1be4b0ad00..0e8a46d57e 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,7 +47,7 @@ class ConfigMapConfigPropertiesTests { @Test void testUseNameAsPrefixUnsetEmptySources() { ConfigMapConfigProperties properties = new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, - "config-map-a", "spring-k8s", false, false, false, RetryProperties.DEFAULT); + "config-map-a", "spring-k8s", false, false, false, RetryProperties.DEFAULT, true); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(1); @@ -73,7 +73,7 @@ void testUseNameAsPrefixUnsetEmptySources() { @Test void testUseNameAsPrefixSetEmptySources() { ConfigMapConfigProperties properties = new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, - "config-map-a", "spring-k8s", true, false, false, RetryProperties.DEFAULT); + "config-map-a", "spring-k8s", true, false, false, RetryProperties.DEFAULT, true); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(1); @@ -104,7 +104,7 @@ void testUseNameAsPrefixUnsetNonEmptySources() { Collections.emptyMap(), null, null, null); ConfigMapConfigProperties properties = new ConfigMapConfigProperties(true, List.of(), List.of(one), Map.of(), - true, "config-map-a", "spring-k8s", true, false, false, RetryProperties.DEFAULT); + true, "config-map-a", "spring-k8s", true, false, false, RetryProperties.DEFAULT, true); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(1); @@ -147,7 +147,7 @@ void testUseNameAsPrefixSetNonEmptySources() { Collections.emptyMap(), null, true, null); ConfigMapConfigProperties properties = new ConfigMapConfigProperties(true, List.of(), List.of(one, two, three), - Map.of(), true, "config-map-a", "spring-k8s", true, false, false, RetryProperties.DEFAULT); + Map.of(), true, "config-map-a", "spring-k8s", true, false, false, RetryProperties.DEFAULT, true); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(3); @@ -198,7 +198,7 @@ void testMultipleCases() { ConfigMapConfigProperties properties = new ConfigMapConfigProperties(true, List.of(), List.of(one, two, three, four), Map.of(), true, "config-map-a", "spring-k8s", true, false, false, - RetryProperties.DEFAULT); + RetryProperties.DEFAULT, true); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(4); @@ -231,7 +231,7 @@ void testMultipleCases() { void testUseIncludeProfileSpecificSourcesNoChanges() { ConfigMapConfigProperties properties = new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, - "config-map-a", "spring-k8s", false, true, false, RetryProperties.DEFAULT); + "config-map-a", "spring-k8s", false, true, false, RetryProperties.DEFAULT, true); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(1); @@ -260,7 +260,7 @@ void testUseIncludeProfileSpecificSourcesNoChanges() { void testUseIncludeProfileSpecificSourcesDefaultChanged() { ConfigMapConfigProperties properties = new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, - "config-map-a", "spring-k8s", false, false, false, RetryProperties.DEFAULT); + "config-map-a", "spring-k8s", false, false, false, RetryProperties.DEFAULT, true); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(1); @@ -304,7 +304,7 @@ void testUseIncludeProfileSpecificSourcesDefaultChangedSourceOverride() { Collections.emptyMap(), null, null, false); ConfigMapConfigProperties properties = new ConfigMapConfigProperties(true, List.of(), List.of(one, two, three), - Map.of(), true, "config-map-a", "spring-k8s", false, false, false, RetryProperties.DEFAULT); + Map.of(), true, "config-map-a", "spring-k8s", false, false, false, RetryProperties.DEFAULT, true); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(3); @@ -362,7 +362,7 @@ void testLabelsMultipleCases() { ConfigMapConfigProperties properties = new ConfigMapConfigProperties(true, List.of(), List.of(one, two, three, four), Map.of(), true, "config-map-a", "spring-k8s", false, false, false, - RetryProperties.DEFAULT); + RetryProperties.DEFAULT, true); List sources = properties.determineSources(new MockEnvironment()); // we get 8 property sources, since "named" ones with "application" are diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigUtilsTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigUtilsTests.java index 6a40e4f0cc..74ba96896b 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigUtilsTests.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigUtilsTests.java @@ -19,7 +19,6 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -215,7 +214,7 @@ void testIssue1757() { Map.of("client-id", "clientB", "client-secret", "b")); MultipleSourcesContainer container = ConfigUtils.processLabeledData(List.of(containerA, containerB), - new MockEnvironment(), Map.of("load", "true"), "default", Set.of(), false); + new MockEnvironment(), Map.of("load", "true"), "default", false); System.out.println(container); assertThat(container.names()).containsExactlyInAnyOrder("client-1", "client-2"); diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigDataLocationResolverTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigDataLocationResolverTests.java index 852d981ad9..68778eabff 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigDataLocationResolverTests.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigDataLocationResolverTests.java @@ -228,10 +228,10 @@ void testResolveProfileSpecificFour() { // 'one' and 'two' prove that we have not registered ConfigMapConfigProperties and // SecretsConfigProperties in the bootstrap context ConfigMapConfigProperties one = new ConfigMapConfigProperties(false, List.of(), List.of(), Map.of(), false, - null, null, false, false, false, null); + null, null, false, false, false, null, true); SecretsConfigProperties two = new SecretsConfigProperties(false, Map.of(), List.of(), List.of(), false, null, - null, false, false, false, null); + null, false, false, false, null, true); KubernetesClientProperties kubernetesClientProperties = RESOLVER_CONTEXT.getBootstrapContext() .get(KubernetesClientProperties.class); 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 index d2b36f0260..08c71c904f 100644 --- 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 @@ -31,8 +31,8 @@ class LabeledSecretNormalizedSourceTests { @Test void testEqualsAndHashCode() { - LabeledSecretNormalizedSource left = new LabeledSecretNormalizedSource("namespace", labels, false, false); - LabeledSecretNormalizedSource right = new LabeledSecretNormalizedSource("namespace", labels, true, false); + LabeledSecretNormalizedSource left = new LabeledSecretNormalizedSource("namespace", labels, false); + LabeledSecretNormalizedSource right = new LabeledSecretNormalizedSource("namespace", labels, true); Assertions.assertThat(left.hashCode()).isEqualTo(right.hashCode()); Assertions.assertThat(left).isEqualTo(right); @@ -48,10 +48,8 @@ void testEqualsAndHashCodePrefixDoesNotMatter() { ConfigUtils.Prefix knownLeft = ConfigUtils.findPrefix("left", false, false, "some"); ConfigUtils.Prefix knownRight = ConfigUtils.findPrefix("right", false, false, "some"); - LabeledSecretNormalizedSource left = new LabeledSecretNormalizedSource("namespace", labels, true, knownLeft, - false); - LabeledSecretNormalizedSource right = new LabeledSecretNormalizedSource("namespace", labels, true, knownRight, - false); + LabeledSecretNormalizedSource left = new LabeledSecretNormalizedSource("namespace", labels, true, knownLeft); + LabeledSecretNormalizedSource right = new LabeledSecretNormalizedSource("namespace", labels, true, knownRight); Assertions.assertThat(left.hashCode()).isEqualTo(right.hashCode()); Assertions.assertThat(left).isEqualTo(right); @@ -59,40 +57,37 @@ void testEqualsAndHashCodePrefixDoesNotMatter() { @Test void testType() { - LabeledSecretNormalizedSource source = new LabeledSecretNormalizedSource("namespace", labels, false, false); + LabeledSecretNormalizedSource source = new LabeledSecretNormalizedSource("namespace", labels, false); Assertions.assertThat(source.type()).isSameAs(NormalizedSourceType.LABELED_SECRET); } @Test void testImmutableGetLabels() { - LabeledSecretNormalizedSource source = new LabeledSecretNormalizedSource("namespace", labels, false, false); + LabeledSecretNormalizedSource source = new LabeledSecretNormalizedSource("namespace", labels, false); Assertions.assertThatThrownBy(() -> source.labels().put("c", "d")).isInstanceOf(RuntimeException.class); } @Test void testTarget() { - LabeledSecretNormalizedSource source = new LabeledSecretNormalizedSource("namespace", labels, false, false); + LabeledSecretNormalizedSource source = new LabeledSecretNormalizedSource("namespace", labels, false); Assertions.assertThat(source.target()).isEqualTo("secret"); } @Test void testConstructorFields() { ConfigUtils.Prefix prefix = ConfigUtils.findPrefix("prefix", false, false, "some"); - LabeledSecretNormalizedSource source = new LabeledSecretNormalizedSource("namespace", labels, false, prefix, - true); + LabeledSecretNormalizedSource source = new LabeledSecretNormalizedSource("namespace", labels, false, prefix); Assertions.assertThat(source.name().isEmpty()).isTrue(); Assertions.assertThat(source.namespace().get()).isEqualTo("namespace"); Assertions.assertThat(source.failFast()).isFalse(); - Assertions.assertThat(source.profileSpecificSources()).isTrue(); } @Test void testConstructorWithoutPrefixFields() { - LabeledSecretNormalizedSource source = new LabeledSecretNormalizedSource("namespace", labels, true, true); + LabeledSecretNormalizedSource source = new LabeledSecretNormalizedSource("namespace", labels, true); Assertions.assertThat(source.namespace().get()).isEqualTo("namespace"); Assertions.assertThat(source.failFast()).isTrue(); Assertions.assertThat(ConfigUtils.Prefix.DEFAULT).isSameAs(source.prefix()); - Assertions.assertThat(source.profileSpecificSources()).isTrue(); } } diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SecretsConfigPropertiesBindingTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SecretsConfigPropertiesBindingTests.java index 94de9a44df..a9f419c00e 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SecretsConfigPropertiesBindingTests.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SecretsConfigPropertiesBindingTests.java @@ -46,6 +46,7 @@ void testWithDefaults() { Assertions.assertThat(props.useNameAsPrefix()).isFalse(); Assertions.assertThat(props.includeProfileSpecificSources()).isTrue(); Assertions.assertThat(props.failFast()).isFalse(); + Assertions.assertThat(props.namespacedBatchRead()).isTrue(); Assertions.assertThat(props.retry()).isNotNull(); Assertions.assertThat(props.retry().initialInterval()).isEqualTo(1000L); @@ -77,7 +78,8 @@ void testWithNonDefaults() { "spring.cloud.kubernetes.secrets.retry.multiplier=1.2", "spring.cloud.kubernetes.secrets.retry.max-interval=3", "spring.cloud.kubernetes.secrets.retry.max-attempts=4", - "spring.cloud.kubernetes.secrets.retry.enabled=false") + "spring.cloud.kubernetes.secrets.retry.enabled=false", + "spring.cloud.kubernetes.secrets.namespaced-batch-read=false") .run(context -> { SecretsConfigProperties props = context.getBean(SecretsConfigProperties.class); Assertions.assertThat(props).isNotNull(); @@ -96,6 +98,7 @@ void testWithNonDefaults() { Assertions.assertThat(source.explicitPrefix()).isEqualTo("source-prefix"); Assertions.assertThat(source.useNameAsPrefix()).isTrue(); Assertions.assertThat(source.includeProfileSpecificSources()).isTrue(); + Assertions.assertThat(props.namespacedBatchRead()).isFalse(); Assertions.assertThat(props.labels().size()).isEqualTo(1); Assertions.assertThat(props.labels().get("label-a")).isEqualTo("label-a"); 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 index 3ce6463ad8..91a85bcdfa 100644 --- 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 @@ -39,7 +39,7 @@ class SecretsConfigPropertiesTests { void emptySourcesSecretName() { SecretsConfigProperties properties = new SecretsConfigProperties(false, Map.of(), List.of(), List.of(), true, - null, "namespace", false, true, false, RetryProperties.DEFAULT); + null, "namespace", false, true, false, RetryProperties.DEFAULT, true); List source = properties.determineSources(new MockEnvironment()); Assertions.assertThat(source.size()).isEqualTo(1); @@ -80,7 +80,7 @@ void multipleSources() { Map.of("three", "3"), null, false, false); SecretsConfigProperties properties = new SecretsConfigProperties(false, Map.of(), List.of(), - List.of(one, two, three), true, null, "namespace", false, true, false, RetryProperties.DEFAULT); + List.of(one, two, three), true, null, "namespace", false, true, false, RetryProperties.DEFAULT, true); List result = properties.determineSources(new MockEnvironment()); Assertions.assertThat(result.size()).isEqualTo(6); @@ -122,7 +122,7 @@ void multipleSources() { void testUseNameAsPrefixUnsetEmptySources() { SecretsConfigProperties properties = new SecretsConfigProperties(false, Map.of(), List.of(), List.of(), true, - "secret-a", "namespace", false, true, false, RetryProperties.DEFAULT); + "secret-a", "namespace", false, true, false, RetryProperties.DEFAULT, true); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(1); @@ -149,7 +149,7 @@ void testUseNameAsPrefixUnsetEmptySources() { void testUseNameAsPrefixSetEmptySources() { SecretsConfigProperties properties = new SecretsConfigProperties(false, Map.of(), List.of(), List.of(), true, - "secret-a", "namespace", true, true, false, RetryProperties.DEFAULT); + "secret-a", "namespace", true, true, false, RetryProperties.DEFAULT, true); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(1); @@ -180,7 +180,7 @@ void testUseNameAsPrefixUnsetNonEmptySources() { null, true, false); SecretsConfigProperties properties = new SecretsConfigProperties(false, Map.of(), List.of(), List.of(one), true, - "secret-one", null, false, true, false, RetryProperties.DEFAULT); + "secret-one", null, false, true, false, RetryProperties.DEFAULT, true); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(1); @@ -223,7 +223,7 @@ void testUseNameAsPrefixSetNonEmptySources() { Map.of(), null, true, false); SecretsConfigProperties properties = new SecretsConfigProperties(false, Map.of(), List.of(), - List.of(one, two, three), true, "secret-one", null, false, true, false, RetryProperties.DEFAULT); + List.of(one, two, three), true, "secret-one", null, false, true, false, RetryProperties.DEFAULT, true); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(3); @@ -274,7 +274,7 @@ void testMultipleCases() { SecretsConfigProperties properties = new SecretsConfigProperties(false, Map.of(), List.of(), List.of(one, two, three, four), true, "secret-one", "spring-k8s", false, false, false, - RetryProperties.DEFAULT); + RetryProperties.DEFAULT, true); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(4); @@ -307,7 +307,6 @@ void testMultipleCases() { * - labels: * - name: second-label * value: secret-two - * includeProfileSpecificSources: true * useNameAsPrefix: true * explicitPrefix: two * - labels: @@ -336,8 +335,8 @@ void testLabelsMultipleCases() { Map.of("fourth-label", "secret-four"), null, false, false); SecretsConfigProperties properties = new SecretsConfigProperties(false, Map.of(), List.of(), - List.of(one, two, three, four), false, null, "spring-k8s", false, false, false, - RetryProperties.DEFAULT); + List.of(one, two, three, four), false, null, "spring-k8s", false, false, false, RetryProperties.DEFAULT, + true); List sources = properties.determineSources(new MockEnvironment()); // we get 8 property sources, since "named" ones with "application" are @@ -348,19 +347,15 @@ void testLabelsMultipleCases() { LabeledSecretNormalizedSource labeled1 = (LabeledSecretNormalizedSource) sources.get(1); Assertions.assertThat(labeled1.prefix().prefixProvider().get()).isEqualTo("one"); - Assertions.assertThat(labeled1.profileSpecificSources()).isFalse(); LabeledSecretNormalizedSource labeled3 = (LabeledSecretNormalizedSource) sources.get(3); Assertions.assertThat(labeled3.prefix().prefixProvider().get()).isEqualTo("two"); - Assertions.assertThat(labeled3.profileSpecificSources()).isTrue(); LabeledSecretNormalizedSource labeled5 = (LabeledSecretNormalizedSource) sources.get(5); Assertions.assertThat(labeled5.prefix().prefixProvider().get()).isEqualTo("three"); - Assertions.assertThat(labeled5.profileSpecificSources()).isFalse(); LabeledSecretNormalizedSource labeled7 = (LabeledSecretNormalizedSource) sources.get(7); Assertions.assertThat(labeled7.prefix()).isSameAs(ConfigUtils.Prefix.DEFAULT); - Assertions.assertThat(labeled7.profileSpecificSources()).isFalse(); Set set = new LinkedHashSet<>(sources); Assertions.assertThat(set.size()).isEqualTo(5); 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 88905606bc..30b88c6448 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 @@ -91,7 +91,7 @@ public KubernetesPropertySourceSupplier configMapPropertySourceSupplier( NamedConfigMapNormalizedSource source = new NamedConfigMapNormalizedSource(applicationName, space, false, ConfigUtils.Prefix.DEFAULT, true, true); KubernetesClientConfigContext context = new KubernetesClientConfigContext(coreApi, source, space, - springEnv, false); + springEnv, false, true); propertySources.add(new KubernetesClientConfigMapPropertySource(context)); }); @@ -111,7 +111,7 @@ public KubernetesPropertySourceSupplier secretsPropertySourceSupplier(Kubernetes NormalizedSource source = new NamedSecretNormalizedSource(applicationName, space, false, ConfigUtils.Prefix.DEFAULT, true, true); KubernetesClientConfigContext context = new KubernetesClientConfigContext(coreApi, source, space, - springEnv, false); + springEnv, false, true); propertySources.add(new KubernetesClientSecretsPropertySource(context)); }); 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 6fb2ec39b9..2338c285f8 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 @@ -30,14 +30,14 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; 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.KubernetesClientConfigMapsCache; -import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsCache; import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsPropertySource; +import org.springframework.cloud.kubernetes.client.config.KubernetesClientSourcesNamespaceBatched; import org.springframework.cloud.kubernetes.commons.config.ConfigUtils; import org.springframework.cloud.kubernetes.commons.config.Constants; import org.springframework.cloud.kubernetes.commons.config.NamedConfigMapNormalizedSource; @@ -123,13 +123,13 @@ static void before() { NormalizedSource defaultSource = new NamedConfigMapNormalizedSource(applicationName, "default", false, true); KubernetesClientConfigContext defaultContext = new KubernetesClientConfigContext(coreApi, defaultSource, - "default", springEnv); + "default", springEnv, true, true); propertySources.add(new KubernetesClientConfigMapPropertySource(defaultContext)); if ("stores".equals(applicationName) && "dev".equals(namespace)) { NormalizedSource devSource = new NamedConfigMapNormalizedSource(applicationName, "dev", false, true); KubernetesClientConfigContext devContext = new KubernetesClientConfigContext(coreApi, devSource, "dev", - springEnv); + springEnv, true, true); propertySources.add(new KubernetesClientConfigMapPropertySource(devContext)); } return propertySources; @@ -139,7 +139,7 @@ static void before() { NormalizedSource source = new NamedSecretNormalizedSource(applicationName, "default", false, true); KubernetesClientConfigContext context = new KubernetesClientConfigContext(coreApi, source, "default", - springEnv); + springEnv, true, true); propertySources.add(new KubernetesClientSecretsPropertySource(context)); return propertySources; @@ -147,13 +147,19 @@ static void before() { } @AfterEach - void after() { - new KubernetesClientConfigMapsCache().discardAll(); - new KubernetesClientSecretsCache().discardAll(); + void afterEach() { + new KubernetesClientSourcesNamespaceBatched().discardConfigMaps(); + new KubernetesClientSourcesNamespaceBatched().discardSecrets(); + } + + @BeforeEach + void beforeEach() { + new KubernetesClientSourcesNamespaceBatched().discardConfigMaps(); + new KubernetesClientSourcesNamespaceBatched().discardSecrets(); } @Test - public void testApplicationCase() throws ApiException { + void testApplicationCase() throws ApiException { CoreV1Api coreApi = mock(CoreV1Api.class); when(coreApi.listNamespacedConfigMap(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null))) @@ -276,7 +282,7 @@ public void testStoresCase() throws ApiException { } @Test - public void testStoresProfileCase() throws ApiException { + void testStoresProfileCase() throws ApiException { CoreV1Api coreApi = mock(CoreV1Api.class); when(coreApi.listNamespacedConfigMap(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null))) @@ -340,7 +346,7 @@ else if (propertySource.getName().equals("secret.stores.default")) { } @Test - public void testApplicationPropertiesAnSecretsOverride() throws ApiException { + void testApplicationPropertiesAnSecretsOverride() throws ApiException { CoreV1Api coreApi = mock(CoreV1Api.class); when(coreApi.listNamespacedConfigMap(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null))) @@ -381,7 +387,7 @@ public void testApplicationPropertiesAnSecretsOverride() throws ApiException { } @Test - public void testSingleConfigMapMultipleSources() throws ApiException { + void testSingleConfigMapMultipleSources() throws ApiException { CoreV1Api coreApi = mock(CoreV1Api.class); when(coreApi.listNamespacedConfigMap(eq("default"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null))) @@ -395,7 +401,7 @@ public void testSingleConfigMapMultipleSources() throws ApiException { NormalizedSource devSource = new NamedConfigMapNormalizedSource(name, namespace, false, ConfigUtils.Prefix.DEFAULT, true, true); KubernetesClientConfigContext devContext = new KubernetesClientConfigContext(coreApi, devSource, "default", - environment); + environment, true, true); propertySources.add(new KubernetesClientConfigMapPropertySource(devContext)); return propertySources; }); diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesPropertySourceSupplierTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesPropertySourceSupplierTests.java index a6aaba092c..0c58618750 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesPropertySourceSupplierTests.java +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/KubernetesPropertySourceSupplierTests.java @@ -30,12 +30,12 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.cloud.config.environment.Environment; -import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapsCache; -import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsCache; +import org.springframework.cloud.kubernetes.client.config.KubernetesClientSourcesNamespaceBatched; import org.springframework.cloud.kubernetes.commons.config.Constants; import static org.assertj.core.api.Assertions.assertThat; @@ -102,8 +102,14 @@ static void afterAll() { @AfterEach void afterEach() { - new KubernetesClientConfigMapsCache().discardAll(); - new KubernetesClientSecretsCache().discardAll(); + new KubernetesClientSourcesNamespaceBatched().discardConfigMaps(); + new KubernetesClientSourcesNamespaceBatched().discardSecrets(); + } + + @BeforeEach + void beforeEach() { + new KubernetesClientSourcesNamespaceBatched().discardConfigMaps(); + new KubernetesClientSourcesNamespaceBatched().discardSecrets(); } @Test diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/it/CompositeKubernetesIntegrationTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/it/CompositeKubernetesIntegrationTests.java index fd1d270b51..4871f6688f 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/it/CompositeKubernetesIntegrationTests.java +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/it/CompositeKubernetesIntegrationTests.java @@ -18,7 +18,6 @@ import java.util.Map; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -28,7 +27,6 @@ import org.springframework.cloud.config.environment.Environment; import org.springframework.cloud.config.environment.PropertySource; import org.springframework.cloud.config.server.environment.NativeEnvironmentRepository; -import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapsCache; import org.springframework.cloud.kubernetes.configserver.KubernetesConfigServerApplication; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; @@ -46,11 +44,6 @@ */ class CompositeKubernetesIntegrationTests { - @AfterEach - void after() { - new KubernetesClientConfigMapsCache().discardAll(); - } - @Nested @SpringBootTest(classes = { KubernetesConfigServerApplication.class }, properties = { "spring.main.cloud-platform=KUBERNETES", "spring.cloud.kubernetes.client.namespace=default", diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/it/ConfigServerIntegration.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/it/ConfigServerIntegration.java index eea41166af..944620b99f 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/it/ConfigServerIntegration.java +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/it/ConfigServerIntegration.java @@ -36,8 +36,6 @@ import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.cloud.config.environment.Environment; import org.springframework.cloud.config.environment.PropertySource; -import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapsCache; -import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsCache; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.get; @@ -131,9 +129,6 @@ void afterEach() { WireMock.shutdownServer(); wireMockServer.stop(); wireMockServer.shutdownServer(); - - new KubernetesClientConfigMapsCache().discardAll(); - new KubernetesClientSecretsCache().discardAll(); } @Test 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 index 0e51ac0ce9..517a0d0569 100644 --- 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 @@ -28,5 +28,5 @@ * @author wind57 */ record Fabric8ConfigContext(KubernetesClient client, NormalizedSource normalizedSource, String namespace, - Environment environment) { + Environment environment, boolean namespacedBatchRead) { } diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigDataLocationResolver.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigDataLocationResolver.java index 278541ea66..afa7e92d70 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigDataLocationResolver.java +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigDataLocationResolver.java @@ -65,7 +65,7 @@ protected void registerBeans(ConfigDataLocationResolverContext resolverContext, kubernetesClient, configMapProperties, namespaceProvider); if (isRetryEnabledForConfigMap(configMapProperties)) { configMapPropertySourceLocator = new ConfigDataRetryableConfigMapPropertySourceLocator( - configMapPropertySourceLocator, configMapProperties, new Fabric8ConfigMapsCache()); + configMapPropertySourceLocator, configMapProperties, new Fabric8SourcesNamespaceBatched()); } registerSingle(bootstrapContext, ConfigMapPropertySourceLocator.class, configMapPropertySourceLocator, @@ -77,7 +77,7 @@ protected void registerBeans(ConfigDataLocationResolverContext resolverContext, kubernetesClient, secretsProperties, namespaceProvider); if (isRetryEnabledForSecrets(secretsProperties)) { secretsPropertySourceLocator = new ConfigDataRetryableSecretsPropertySourceLocator( - secretsPropertySourceLocator, secretsProperties, new Fabric8SecretsCache()); + secretsPropertySourceLocator, secretsProperties, new Fabric8SourcesNamespaceBatched()); } registerSingle(bootstrapContext, SecretsPropertySourceLocator.class, secretsPropertySourceLocator, 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 2231293e7e..bf9a883148 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 @@ -45,19 +45,20 @@ public class Fabric8ConfigMapPropertySourceLocator extends ConfigMapPropertySour Fabric8ConfigMapPropertySourceLocator(KubernetesClient client, ConfigMapConfigProperties properties, KubernetesNamespaceProvider provider) { - super(properties, new Fabric8ConfigMapsCache()); + super(properties, new Fabric8SourcesNamespaceBatched()); this.client = client; this.provider = provider; } @Override protected MapPropertySource getMapPropertySource(NormalizedSource normalizedSource, - ConfigurableEnvironment environment) { + ConfigurableEnvironment environment, boolean namespacedBatchRead) { // 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); + Fabric8ConfigContext context = new Fabric8ConfigContext(client, normalizedSource, namespace, environment, + namespacedBatchRead); 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 9c533d473e..c77271ac17 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 @@ -33,6 +33,11 @@ import org.springframework.cloud.kubernetes.fabric8.Fabric8Utils; import org.springframework.core.env.Environment; +import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8SourcesNamespaceBatched.strippedConfigMapsBatchRead; +import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8SourcesNamespaceBatched.strippedSecretsBatchRead; +import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8SourcesNonNamespaceBatched.strippedConfigMapsNonBatchRead; +import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8SourcesNonNamespaceBatched.strippedSecretsNonBatchRead; + /** * Utility class that works with configuration properties. * @@ -58,43 +63,29 @@ public static Set namespaces(KubernetesClient client, KubernetesNamespac return namespaces; } - /** - *
-	 *     1. read all secrets in the provided namespace
-	 *     2. from the above, filter the ones that we care about (filter by labels)
-	 *     3. with secret names from (2), find out if there are any profile based secrets (if profiles is not empty)
-	 *     4. concat (2) and (3) and these are the secrets we are interested in
-	 *     5. see if any of the secrets from (4) has a single yaml/properties file
-	 *     6. gather all the names of the secrets (from 4) + data they hold
-	 * 
- */ - static MultipleSourcesContainer secretsDataByLabels(KubernetesClient client, String namespace, - Map labels, Environment environment, Set profiles) { - List strippedSecrets = strippedSecrets(client, namespace); - if (strippedSecrets.isEmpty()) { - return MultipleSourcesContainer.empty(); - } - return ConfigUtils.processLabeledData(strippedSecrets, environment, labels, namespace, profiles, true); - } - /** *
 	 *     1. read all config maps in the provided namespace
-	 *     2. from the above, filter the ones that we care about (filter by labels)
-	 *     3. with config maps names from (2), find out if there are any profile based ones (if profiles is not empty)
-	 *     4. concat (2) and (3) and these are the config maps we are interested in
-	 *     5. see if any from (4) has a single yaml/properties file
-	 *     6. gather all the names of the config maps (from 4) + data they hold
+	 *     2. from the above, filter the ones that we care about (by name)
+	 *     3. see if any of the config maps has a single yaml/properties file
+	 *     4. gather all the names of the config maps + data they hold
 	 * 
*/ - static MultipleSourcesContainer configMapsDataByLabels(KubernetesClient client, String namespace, - Map labels, Environment environment, Set profiles) { - List strippedConfigMaps = strippedConfigMaps(client, namespace); - if (strippedConfigMaps.isEmpty()) { - return MultipleSourcesContainer.empty(); + static MultipleSourcesContainer configMapsDataByName(KubernetesClient client, String namespace, + LinkedHashSet sourceNames, Environment environment, boolean namespacedBatchRead) { + + List strippedConfigMaps; + + if (namespacedBatchRead) { + LOG.debug("Will read all configmaps in namespace : " + namespace); + strippedConfigMaps = strippedConfigMapsBatchRead(client, namespace); + } + else { + LOG.debug("Will read individual configmaps in namespace : " + namespace + " with names : " + sourceNames); + strippedConfigMaps = strippedConfigMapsNonBatchRead(client, namespace, sourceNames); } - return ConfigUtils.processLabeledData(strippedConfigMaps, environment, labels, namespace, profiles, false); + return ConfigUtils.processNamedData(strippedConfigMaps, environment, sourceNames, namespace, false); } /** @@ -106,47 +97,70 @@ static MultipleSourcesContainer configMapsDataByLabels(KubernetesClient client, * */ static MultipleSourcesContainer secretsDataByName(KubernetesClient client, String namespace, - LinkedHashSet sourceNames, Environment environment) { - List strippedSecrets = strippedSecrets(client, namespace); - if (strippedSecrets.isEmpty()) { - return MultipleSourcesContainer.empty(); + LinkedHashSet sourceNames, Environment environment, boolean namespacedBatchRead) { + + List strippedSecrets; + + if (namespacedBatchRead) { + LOG.debug("Will read all secrets in namespace : " + namespace); + strippedSecrets = strippedSecretsBatchRead(client, namespace); } + else { + LOG.debug("Will read individual secrets in namespace : " + namespace + " with names : " + sourceNames); + strippedSecrets = strippedSecretsNonBatchRead(client, namespace, sourceNames); + } + return ConfigUtils.processNamedData(strippedSecrets, environment, sourceNames, namespace, true); } /** *
 	 *     1. read all config maps in the provided namespace
-	 *     2. from the above, filter the ones that we care about (by name)
-	 *     3. see if any of the config maps has a single yaml/properties file
+	 *     2. from the above, filter the ones that we care about (filter by labels)
+	 *     3. see if any from (2) has a single yaml/properties file
 	 *     4. gather all the names of the config maps + data they hold
 	 * 
*/ - static MultipleSourcesContainer configMapsDataByName(KubernetesClient client, String namespace, - LinkedHashSet sourceNames, Environment environment) { - List strippedConfigMaps = strippedConfigMaps(client, namespace); - if (strippedConfigMaps.isEmpty()) { - return MultipleSourcesContainer.empty(); - } - return ConfigUtils.processNamedData(strippedConfigMaps, environment, sourceNames, namespace, false); - } + static MultipleSourcesContainer configMapsDataByLabels(KubernetesClient client, String namespace, + Map labels, Environment environment, boolean namespacedBatchRead) { + + List strippedConfigMaps; - private static List strippedConfigMaps(KubernetesClient client, String namespace) { - List strippedConfigMaps = Fabric8ConfigMapsCache.byNamespace(client, namespace); - if (strippedConfigMaps.isEmpty()) { - LOG.debug("No configmaps in namespace '" + namespace + "'"); + if (namespacedBatchRead) { + LOG.debug("Will read all configmaps in namespace : " + namespace); + strippedConfigMaps = strippedConfigMapsBatchRead(client, namespace); + } + else { + LOG.debug("Will read individual configmaps in namespace : " + namespace + " with labels : " + labels); + strippedConfigMaps = strippedConfigMapsNonBatchRead(client, namespace, labels); } - return strippedConfigMaps; + return ConfigUtils.processLabeledData(strippedConfigMaps, environment, labels, namespace, false); } - private static List strippedSecrets(KubernetesClient client, String namespace) { - List strippedSecrets = Fabric8SecretsCache.byNamespace(client, namespace); - if (strippedSecrets.isEmpty()) { - LOG.debug("No secrets in namespace '" + namespace + "'"); + /** + *
+	 *     1. read all secrets in the provided namespace
+	 *     2. from the above, filter the ones that we care about (filter by labels)
+	 *     3. see if any of the secrets from (2) has a single yaml/properties file
+	 *     4. gather all the names of the secrets + data they hold
+	 * 
+ */ + static MultipleSourcesContainer secretsDataByLabels(KubernetesClient client, String namespace, + Map labels, Environment environment, boolean namespacedBatchRead) { + + List strippedSecrets; + + if (namespacedBatchRead) { + LOG.debug("Will read all secrets in namespace : " + namespace); + strippedSecrets = strippedSecretsBatchRead(client, namespace); + } + else { + LOG.debug("Will read individual secrets in namespace : " + namespace + " with labels : " + labels); + strippedSecrets = strippedSecretsNonBatchRead(client, namespace, labels); } - return strippedSecrets; + return ConfigUtils.processLabeledData(strippedSecrets, environment, labels, namespace, true); } } diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsCache.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsCache.java deleted file mode 100644 index 9dd494fc25..0000000000 --- a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsCache.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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.List; -import java.util.concurrent.ConcurrentHashMap; - -import io.fabric8.kubernetes.api.model.Secret; -import io.fabric8.kubernetes.client.KubernetesClient; -import org.apache.commons.logging.LogFactory; - -import org.springframework.cloud.kubernetes.commons.config.SecretsCache; -import org.springframework.cloud.kubernetes.commons.config.StrippedSourceContainer; -import org.springframework.core.log.LogAccessor; - -/** - * A cache of ConfigMaps per namespace. Makes sure we read config maps only once from a - * namespace. - * - * @author wind57 - */ -final class Fabric8SecretsCache implements SecretsCache { - - private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(Fabric8SecretsCache.class)); - - /** - * at the moment our loading of config maps is using a single thread, but might change - * in the future, thus a thread safe structure. - */ - private static final ConcurrentHashMap> CACHE = new ConcurrentHashMap<>(); - - @Override - public void discardAll() { - CACHE.clear(); - } - - static List byNamespace(KubernetesClient client, String namespace) { - boolean[] b = new boolean[1]; - List result = CACHE.computeIfAbsent(namespace, x -> { - b[0] = true; - return strippedSecrets(client.secrets().inNamespace(namespace).list().getItems()); - }); - - if (b[0]) { - LOG.debug(() -> "Loaded all secrets in namespace '" + namespace + "'"); - } - else { - LOG.debug(() -> "Loaded (from cache) all secrets in namespace '" + namespace + "'"); - } - - return result; - } - - private static List strippedSecrets(List secrets) { - return secrets.stream() - .map(secret -> new StrippedSourceContainer(secret.getMetadata().getLabels(), secret.getMetadata().getName(), - secret.getData())) - .toList(); - } - -} 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 2d098ebfab..f7a1a765ec 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 @@ -45,19 +45,20 @@ public class Fabric8SecretsPropertySourceLocator extends SecretsPropertySourceLo Fabric8SecretsPropertySourceLocator(KubernetesClient client, SecretsConfigProperties properties, KubernetesNamespaceProvider provider) { - super(properties, new Fabric8SecretsCache()); + super(properties, new Fabric8SourcesNamespaceBatched()); this.client = client; this.provider = provider; } @Override protected SecretsPropertySource getPropertySource(ConfigurableEnvironment environment, - NormalizedSource normalizedSource) { + NormalizedSource normalizedSource, boolean namespacedBatchRead) { // 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); + Fabric8ConfigContext context = new Fabric8ConfigContext(client, normalizedSource, namespace, environment, + namespacedBatchRead); return new Fabric8SecretsPropertySource(context); } diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapsCache.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SourcesNamespaceBatched.java similarity index 55% rename from spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapsCache.java rename to spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SourcesNamespaceBatched.java index f7c18ce149..c5abbafc98 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapsCache.java +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SourcesNamespaceBatched.java @@ -19,11 +19,11 @@ import java.util.List; import java.util.concurrent.ConcurrentHashMap; -import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.client.KubernetesClient; import org.apache.commons.logging.LogFactory; import org.springframework.cloud.kubernetes.commons.config.ConfigMapCache; +import org.springframework.cloud.kubernetes.commons.config.SecretsCache; import org.springframework.cloud.kubernetes.commons.config.StrippedSourceContainer; import org.springframework.core.log.LogAccessor; @@ -33,26 +33,34 @@ * * @author wind57 */ -final class Fabric8ConfigMapsCache implements ConfigMapCache { +final class Fabric8SourcesNamespaceBatched implements SecretsCache, ConfigMapCache { - private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(Fabric8ConfigMapsCache.class)); + private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(Fabric8SourcesNamespaceBatched.class)); /** * at the moment our loading of config maps is using a single thread, but might change * in the future, thus a thread safe structure. */ - private static final ConcurrentHashMap> CACHE = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap> SECRETS_CACHE = new ConcurrentHashMap<>(); + + private static final ConcurrentHashMap> CONFIG_MAPS_CACHE = new ConcurrentHashMap<>(); + + @Override + public void discardSecrets() { + SECRETS_CACHE.clear(); + } @Override - public void discardAll() { - CACHE.clear(); + public void discardConfigMaps() { + CONFIG_MAPS_CACHE.clear(); } - static List byNamespace(KubernetesClient client, String namespace) { + static List strippedConfigMapsBatchRead(KubernetesClient client, String namespace) { boolean[] b = new boolean[1]; - List result = CACHE.computeIfAbsent(namespace, x -> { + List result = CONFIG_MAPS_CACHE.computeIfAbsent(namespace, x -> { b[0] = true; - return strippedConfigMaps(client.configMaps().inNamespace(namespace).list().getItems()); + return Fabric8SourcesStripper + .strippedConfigMaps(client.configMaps().inNamespace(namespace).list().getItems()); }); if (b[0]) { @@ -65,11 +73,21 @@ static List byNamespace(KubernetesClient client, String return result; } - private static List strippedConfigMaps(List configMaps) { - return configMaps.stream() - .map(configMap -> new StrippedSourceContainer(configMap.getMetadata().getLabels(), - configMap.getMetadata().getName(), configMap.getData())) - .toList(); + static List strippedSecretsBatchRead(KubernetesClient client, String namespace) { + boolean[] b = new boolean[1]; + List result = SECRETS_CACHE.computeIfAbsent(namespace, x -> { + b[0] = true; + return Fabric8SourcesStripper.strippedSecrets(client.secrets().inNamespace(namespace).list().getItems()); + }); + + if (b[0]) { + LOG.debug(() -> "Loaded all secrets in namespace '" + namespace + "'"); + } + else { + LOG.debug(() -> "Loaded (from cache) all secrets in namespace '" + namespace + "'"); + } + + return result; } } diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SourcesNonNamespaceBatched.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SourcesNonNamespaceBatched.java new file mode 100644 index 0000000000..ad258a2daf --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SourcesNonNamespaceBatched.java @@ -0,0 +1,133 @@ +/* + * Copyright 2013-2024 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.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +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.LogFactory; + +import org.springframework.cloud.kubernetes.commons.config.StrippedSourceContainer; +import org.springframework.core.log.LogAccessor; + +/** + * non batch reads (not reading in the whole namespace) of configmaps and secrets. + * + * @author wind57 + */ +final class Fabric8SourcesNonNamespaceBatched { + + private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(Fabric8SourcesNonNamespaceBatched.class)); + + private Fabric8SourcesNonNamespaceBatched() { + + } + + /** + * read configmaps by name, one by one, without caching them. + */ + static List strippedConfigMapsNonBatchRead(KubernetesClient client, String namespace, + LinkedHashSet sourceNames) { + + List configMaps = new ArrayList<>(sourceNames.size()); + + for (String sourceName : sourceNames) { + ConfigMap configMap = client.configMaps().inNamespace(namespace).withName(sourceName).get(); + if (configMap != null) { + LOG.debug("Loaded config map '" + sourceName + "'"); + configMaps.add(configMap); + } + } + + List strippedConfigMaps = Fabric8SourcesStripper.strippedConfigMaps(configMaps); + + if (strippedConfigMaps.isEmpty()) { + LOG.debug("No configmaps in namespace '" + namespace + "'"); + } + + return strippedConfigMaps; + } + + /** + * read secrets by name, one by one, without caching them. + */ + static List strippedSecretsNonBatchRead(KubernetesClient client, String namespace, + LinkedHashSet sourceNames) { + + List secrets = new ArrayList<>(sourceNames.size()); + + for (String sourceName : sourceNames) { + Secret secret = client.secrets().inNamespace(namespace).withName(sourceName).get(); + if (secret != null) { + LOG.debug("Loaded config map '" + sourceName + "'"); + secrets.add(secret); + } + } + + List strippedSecrets = Fabric8SourcesStripper.strippedSecrets(secrets); + + if (strippedSecrets.isEmpty()) { + LOG.debug("No secrets in namespace '" + namespace + "'"); + } + + return strippedSecrets; + } + + /** + * read configmaps by labels, without caching them. + */ + static List strippedConfigMapsNonBatchRead(KubernetesClient client, String namespace, + Map labels) { + + List configMaps = client.configMaps().inNamespace(namespace).withLabels(labels).list().getItems(); + for (ConfigMap configMap : configMaps) { + LOG.debug("Loaded config map '" + configMap.getMetadata().getName() + "'"); + } + + List strippedConfigMaps = Fabric8SourcesStripper.strippedConfigMaps(configMaps); + if (strippedConfigMaps.isEmpty()) { + LOG.debug("No configmaps in namespace '" + namespace + "'"); + } + + return strippedConfigMaps; + } + + /** + * read secrets by labels, without caching them. + */ + static List strippedSecretsNonBatchRead(KubernetesClient client, String namespace, + Map labels) { + + List secrets = client.secrets().inNamespace(namespace).withLabels(labels).list().getItems(); + for (Secret secret : secrets) { + LOG.debug("Loaded secret '" + secret.getMetadata().getName() + "'"); + } + + List strippedSecrets = Fabric8SourcesStripper.strippedSecrets(secrets); + if (strippedSecrets.isEmpty()) { + LOG.debug("No secrets in namespace '" + namespace + "'"); + } + + return strippedSecrets; + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SourcesStripper.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SourcesStripper.java new file mode 100644 index 0000000000..ceada73543 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SourcesStripper.java @@ -0,0 +1,45 @@ +/* + * Copyright 2013-2024 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.List; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.Secret; + +import org.springframework.cloud.kubernetes.commons.config.StrippedSourceContainer; + +/** + * @author wind57 + */ +interface Fabric8SourcesStripper { + + static List strippedConfigMaps(List configMaps) { + return configMaps.stream() + .map(configMap -> new StrippedSourceContainer(configMap.getMetadata().getLabels(), + configMap.getMetadata().getName(), configMap.getData())) + .toList(); + } + + static List strippedSecrets(List secrets) { + return secrets.stream() + .map(secret -> new StrippedSourceContainer(secret.getMetadata().getLabels(), secret.getMetadata().getName(), + secret.getData())) + .toList(); + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledConfigMapContextToSourceDataProvider.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledConfigMapContextToSourceDataProvider.java index 8058f041dd..4c321fa422 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledConfigMapContextToSourceDataProvider.java +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledConfigMapContextToSourceDataProvider.java @@ -17,7 +17,6 @@ package org.springframework.cloud.kubernetes.fabric8.config; import java.util.Map; -import java.util.Set; import java.util.function.Supplier; import org.springframework.cloud.kubernetes.commons.config.LabeledConfigMapNormalizedSource; @@ -55,13 +54,12 @@ public Fabric8ContextToSourceData get() { return new LabeledSourceData() { @Override - public MultipleSourcesContainer dataSupplier(Map labels, Set profiles) { + public MultipleSourcesContainer dataSupplier(Map labels) { return Fabric8ConfigUtils.configMapsDataByLabels(context.client(), context.namespace(), labels, - context.environment(), profiles); + context.environment(), context.namespacedBatchRead()); } - }.compute(source.labels(), source.prefix(), source.target(), source.profileSpecificSources(), - source.failFast(), context.namespace(), context.environment().getActiveProfiles()); + }.compute(source.labels(), source.prefix(), source.target(), source.failFast(), context.namespace()); }; } 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 index efad27a7ba..4b7e4672c2 100644 --- 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 @@ -17,7 +17,6 @@ package org.springframework.cloud.kubernetes.fabric8.config; import java.util.Map; -import java.util.Set; import java.util.function.Supplier; import org.springframework.cloud.kubernetes.commons.config.LabeledSecretNormalizedSource; @@ -54,13 +53,12 @@ public Fabric8ContextToSourceData get() { return new LabeledSourceData() { @Override - public MultipleSourcesContainer dataSupplier(Map labels, Set profiles) { + public MultipleSourcesContainer dataSupplier(Map labels) { return Fabric8ConfigUtils.secretsDataByLabels(context.client(), context.namespace(), labels, - context.environment(), profiles); + context.environment(), context.namespacedBatchRead()); } - }.compute(source.labels(), source.prefix(), source.target(), source.profileSpecificSources(), - source.failFast(), context.namespace(), context.environment().getActiveProfiles()); + }.compute(source.labels(), source.prefix(), source.target(), source.failFast(), context.namespace()); }; } 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 index 3ac61a7ca5..896d4122eb 100644 --- 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 @@ -63,7 +63,7 @@ protected String generateSourceName(String target, String sourceName, String nam @Override public MultipleSourcesContainer dataSupplier(LinkedHashSet sourceNames) { return Fabric8ConfigUtils.configMapsDataByName(context.client(), context.namespace(), sourceNames, - context.environment()); + context.environment(), context.namespacedBatchRead()); } }.compute(source.name().orElseThrow(), source.prefix(), source.target(), source.profileSpecificSources(), source.failFast(), context.namespace(), context.environment().getActiveProfiles()); 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 index 5a2356085d..764d3a866b 100644 --- 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 @@ -53,7 +53,7 @@ protected String generateSourceName(String target, String sourceName, String nam @Override public MultipleSourcesContainer dataSupplier(LinkedHashSet sourceNames) { return Fabric8ConfigUtils.secretsDataByName(context.client(), context.namespace(), sourceNames, - context.environment()); + context.environment(), context.namespacedBatchRead()); } }.compute(source.name().orElseThrow(), source.prefix(), source.target(), source.profileSpecificSources(), source.failFast(), context.namespace(), context.environment().getActiveProfiles()); 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 b478432e90..f72dfcdbfc 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 @@ -44,7 +44,7 @@ class ConfigMapsTest { @AfterEach void afterEach() { - new Fabric8ConfigMapsCache().discardAll(); + new Fabric8SourcesNamespaceBatched().discardConfigMaps(); } @Test @@ -91,7 +91,7 @@ void testConfigMapFromSingleApplicationProperties() { mockClient.configMaps().inNamespace("test").resource(configMap).create(); NormalizedSource source = new NamedConfigMapNormalizedSource(configMapName, "test", false, false); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "", new MockEnvironment()); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "", new MockEnvironment(), true); Fabric8ConfigMapPropertySource cmps = new Fabric8ConfigMapPropertySource(context); @@ -111,7 +111,7 @@ void testConfigMapFromSingleApplicationYaml() { mockClient.configMaps().inNamespace("test").resource(configMap).create(); NormalizedSource source = new NamedConfigMapNormalizedSource(configMapName, "test", false, false); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "", new MockEnvironment()); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "", new MockEnvironment(), true); Fabric8ConfigMapPropertySource cmps = new Fabric8ConfigMapPropertySource(context); @@ -131,7 +131,7 @@ void testConfigMapFromSingleNonStandardFileName() { mockClient.configMaps().inNamespace("test").resource(configMap).create(); NormalizedSource source = new NamedConfigMapNormalizedSource(configMapName, "test", false, false); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "", new MockEnvironment()); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "", new MockEnvironment(), true); Fabric8ConfigMapPropertySource cmps = new Fabric8ConfigMapPropertySource(context); @@ -151,7 +151,7 @@ void testConfigMapFromSingleInvalidPropertiesContent() { mockClient.configMaps().inNamespace("test").resource(configMap).create(); NormalizedSource source = new NamedConfigMapNormalizedSource(configMapName, "namespace", false, false); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "", new MockEnvironment()); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "", new MockEnvironment(), true); Fabric8ConfigMapPropertySource cmps = new Fabric8ConfigMapPropertySource(context); @@ -169,7 +169,7 @@ void testConfigMapFromSingleInvalidYamlContent() { mockClient.configMaps().inNamespace("test").resource(configMap).create(); NormalizedSource source = new NamedConfigMapNormalizedSource(configMapName, "namespace", false, false); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "", new MockEnvironment()); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "", new MockEnvironment(), true); Fabric8ConfigMapPropertySource cmps = new Fabric8ConfigMapPropertySource(context); @@ -188,7 +188,7 @@ void testConfigMapFromMultipleApplicationProperties() { mockClient.configMaps().inNamespace("test").resource(configMap).create(); NormalizedSource source = new NamedConfigMapNormalizedSource(configMapName, "test", false, false); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "", new MockEnvironment()); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "", new MockEnvironment(), true); Fabric8ConfigMapPropertySource cmps = new Fabric8ConfigMapPropertySource(context); diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/EventBasedConfigurationChangeDetectorTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/EventBasedConfigurationChangeDetectorTests.java index 8c08f84492..ec093076e5 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/EventBasedConfigurationChangeDetectorTests.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/EventBasedConfigurationChangeDetectorTests.java @@ -67,7 +67,7 @@ void verifyConfigChangesAccountsForBootstrapPropertySources() { when(k8sClient.getNamespace()).thenReturn("default"); NormalizedSource source = new NamedConfigMapNormalizedSource("myconfigmap", "default", true, false); - Fabric8ConfigContext context = new Fabric8ConfigContext(k8sClient, source, "default", env); + Fabric8ConfigContext context = new Fabric8ConfigContext(k8sClient, source, "default", env, true); Fabric8ConfigMapPropertySource fabric8ConfigMapPropertySource = new Fabric8ConfigMapPropertySource(context); env.getPropertySources().addFirst(new BootstrapPropertySource<>(fabric8ConfigMapPropertySource)); diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapErrorOnReadingSourceTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapErrorOnReadingSourceTests.java index 9d6be13557..ef8fa1d6eb 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapErrorOnReadingSourceTests.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapErrorOnReadingSourceTests.java @@ -48,6 +48,8 @@ @ExtendWith(OutputCaptureExtension.class) class Fabric8ConfigMapErrorOnReadingSourceTests { + private static final boolean NAMESPACED_BATCHED = true; + private static KubernetesMockServer mockServer; private static KubernetesClient mockClient; @@ -72,7 +74,7 @@ void namedSingleConfigMapFails(CapturedOutput output) { mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once(); ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(), - Map.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT); + Map.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT, NAMESPACED_BATCHED); Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -108,7 +110,7 @@ void namedTwoConfigMapsOneFails(CapturedOutput output) { ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(sourceOne, sourceTwo), Map.of(), true, null, namespace, false, true, false, - RetryProperties.DEFAULT); + RetryProperties.DEFAULT, NAMESPACED_BATCHED); Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -142,7 +144,7 @@ void namedTwoConfigMapsBothFail(CapturedOutput output) { ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(sourceOne, sourceTwo), Map.of(), true, null, namespace, false, true, false, - RetryProperties.DEFAULT); + RetryProperties.DEFAULT, NAMESPACED_BATCHED); Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -173,7 +175,8 @@ void labeledSingleConfigMapFails(CapturedOutput output) { Source configMapSource = new Source(null, namespace, labels, null, null, null); ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), - List.of(configMapSource), labels, true, null, namespace, false, true, false, RetryProperties.DEFAULT); + List.of(configMapSource), labels, true, null, namespace, false, true, false, RetryProperties.DEFAULT, + NAMESPACED_BATCHED); Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -217,7 +220,7 @@ void labeledTwoConfigMapsOneFails(CapturedOutput output) { ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(sourceOne, sourceTwo), Map.of("one", "1", "two", "2"), true, null, namespace, false, true, - false, RetryProperties.DEFAULT); + false, RetryProperties.DEFAULT, NAMESPACED_BATCHED); Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -256,7 +259,7 @@ void labeledTwoConfigMapsBothFail(CapturedOutput output) { ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(sourceOne, sourceTwo), Map.of("one", "1", "two", "2"), true, null, namespace, false, true, - false, RetryProperties.DEFAULT); + false, RetryProperties.DEFAULT, NAMESPACED_BATCHED); Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocatorMockTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocatorMockTests.java index 2e9f1421a7..8dbbcf3feb 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocatorMockTests.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocatorMockTests.java @@ -47,13 +47,13 @@ class Fabric8ConfigMapPropertySourceLocatorMockTests { void constructorWithoutClientNamespaceMustFail() { ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(), - Map.of(), true, "name", null, false, true, false, RetryProperties.DEFAULT); + Map.of(), true, "name", null, false, true, false, RetryProperties.DEFAULT, true); 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())) + assertThatThrownBy(() -> source.getMapPropertySource(normalizedSource, new MockEnvironment(), true)) .isInstanceOf(NamespaceResolutionFailedException.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 9b26bf7f1b..371e2e548b 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 @@ -65,7 +65,7 @@ void locateShouldThrowExceptionOnFailureWhenFailFastIsEnabled() { mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").always(); ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(), - Map.of(), true, name, namespace, false, true, true, RetryProperties.DEFAULT); + Map.of(), true, name, namespace, false, true, true, RetryProperties.DEFAULT, true); Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -83,7 +83,7 @@ void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled(CapturedOutput mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").always(); ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(), - Map.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT); + Map.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT, true); Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceMockTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceMockTests.java index 912ac2d30e..5a489a644d 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceMockTests.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceMockTests.java @@ -38,7 +38,7 @@ void constructorWithClientNamespaceMustNotFail() { Mockito.when(client.getNamespace()).thenReturn("namespace"); NormalizedSource source = new NamedConfigMapNormalizedSource("configmap", null, false, false); - Fabric8ConfigContext context = new Fabric8ConfigContext(client, source, "", new MockEnvironment()); + Fabric8ConfigContext context = new Fabric8ConfigContext(client, source, "", new MockEnvironment(), true); assertThat(new Fabric8ConfigMapPropertySource(context)).isNotNull(); } @@ -47,7 +47,7 @@ void constructorWithNamespaceMustNotFail() { Mockito.when(client.getNamespace()).thenReturn(null); NormalizedSource source = new NamedConfigMapNormalizedSource("configMap", null, false, true); - Fabric8ConfigContext context = new Fabric8ConfigContext(client, source, "", new MockEnvironment()); + Fabric8ConfigContext context = new Fabric8ConfigContext(client, source, "", new MockEnvironment(), true); assertThat(new Fabric8ConfigMapPropertySource(context)).isNotNull(); } 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 8e550838ba..23ddb7373d 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 @@ -50,7 +50,7 @@ void beforeEach() { @AfterEach void afterEach() { - new Fabric8ConfigMapsCache().discardAll(); + new Fabric8SourcesNamespaceBatched().discardConfigMaps(); } @Test @@ -61,7 +61,8 @@ void constructorShouldThrowExceptionOnFailureWhenFailFastIsEnabled() { mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").always(); NormalizedSource source = new NamedConfigMapNormalizedSource(name, namespace, true, DEFAULT, true); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "default", new MockEnvironment()); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "default", new MockEnvironment(), + true); assertThatThrownBy(() -> new Fabric8ConfigMapPropertySource(context)).isInstanceOf(IllegalStateException.class) .hasMessageContaining("v1/namespaces/default/configmaps. Message: Internal Server Error."); } @@ -74,7 +75,7 @@ void constructorShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() { mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").always(); NormalizedSource source = new NamedConfigMapNormalizedSource(name, namespace, false, false); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "default", new MockEnvironment()); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "", new MockEnvironment(), true); assertThatNoException().isThrownBy(() -> new Fabric8ConfigMapPropertySource(context)); } diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigUtilsNamespacedBatchReadTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigUtilsNamespacedBatchReadTests.java new file mode 100644 index 0000000000..0508373152 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigUtilsNamespacedBatchReadTests.java @@ -0,0 +1,419 @@ +/* + * Copyright 2013-2024 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.LinkedHashSet; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.kubernetes.commons.config.MultipleSourcesContainer; +import org.springframework.mock.env.MockEnvironment; + +import static org.springframework.cloud.kubernetes.commons.config.Constants.APPLICATION_YAML; + +/** + * @author wind57 + */ +@EnableKubernetesMockClient(crud = true, https = false) +class Fabric8ConfigUtilsNamespacedBatchReadTests { + + private static final boolean NAMESPACED_BATCH_READ = true; + + private KubernetesClient client; + + @AfterEach + void afterEach() { + new Fabric8SourcesNamespaceBatched().discardSecrets(); + new Fabric8SourcesNamespaceBatched().discardConfigMaps(); + } + + /** + *
+	 *  	- secret 'my-secret' is deployed without any labels
+	 *  	- we search for it by labels 'color=red' and do not find it.
+	 * 
+ */ + @Test + void testSecretDataByLabelsSecretNotFound() { + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder().withMetadata(new ObjectMetaBuilder().withName("my-secret").build()).build()) + .create(); + MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByLabels(client, "spring-k8s", + Map.of("color", "red"), new MockEnvironment(), NAMESPACED_BATCH_READ); + Assertions.assertThat(result.data()).isEmpty(); + Assertions.assertThat(result.names()).isEmpty(); + } + + /** + *
+	 *		- secret 'my-secret' is deployed with label '{color:pink}'
+	 *		- we search for it by same label and find it.
+	 * 
+ */ + @Test + void testSecretDataByLabelsSecretFound() { + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("my-secret").withLabels(Map.of("color", "pink")).build()) + .addToData(Map.of("property", Base64.getEncoder().encodeToString("value".getBytes()))) + .build()) + .create(); + + MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByLabels(client, "spring-k8s", + Map.of("color", "pink"), new MockEnvironment(), NAMESPACED_BATCH_READ); + Assertions.assertThat(result.names()).containsExactlyInAnyOrder("my-secret"); + + @SuppressWarnings("unchecked") + Map data = (Map) result.data().get("my-secret"); + Assertions.assertThat(data).containsExactlyInAnyOrderEntriesOf(Map.of("property", "value")); + } + + /** + *
+	 * 		- secret 'my-secret' is deployed with label '{color:pink}'
+	 * 		- we search for it by same label and find it.
+	 * 		- This secret contains a single .yaml property, as such, it gets some special treatment.
+	 * 
+ */ + @Test + void testSecretDataByLabelsSecretFoundWithPropertyFile() { + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("my-secret").withLabels(Map.of("color", "pink")).build()) + .addToData(Map.of(APPLICATION_YAML, Base64.getEncoder().encodeToString("key1: value1".getBytes()))) + .build()) + .create(); + + MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByLabels(client, "spring-k8s", + Map.of("color", "pink"), new MockEnvironment(), NAMESPACED_BATCH_READ); + Assertions.assertThat(result.names()).containsExactlyInAnyOrder("my-secret"); + + @SuppressWarnings("unchecked") + Map data = (Map) result.data().get("my-secret"); + Assertions.assertThat(data).containsExactlyInAnyOrderEntriesOf(Map.of("key1", "value1")); + } + + /** + *
+	 * 		- secrets 'my-secret' and 'my-secret-2' are deployed with label {color:pink}
+	 * 		- we search for them by same label and find them.
+	 * 
+ */ + @Test + void testSecretDataByLabelsTwoSecretsFound() { + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("my-secret").withLabels(Map.of("color", "pink")).build()) + .addToData(Map.of("property", Base64.getEncoder().encodeToString("value".getBytes()))) + .build()) + .create(); + + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata( + new ObjectMetaBuilder().withName("my-secret-2").withLabels(Map.of("color", "pink")).build()) + .addToData(Map.of("property-2", Base64.getEncoder().encodeToString("value-2".getBytes()))) + .build()) + .create(); + + MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByLabels(client, "spring-k8s", + Map.of("color", "pink"), new MockEnvironment(), NAMESPACED_BATCH_READ); + Assertions.assertThat(result.names()).contains("my-secret"); + Assertions.assertThat(result.names()).contains("my-secret-2"); + + Map data = result.data(); + Assertions.assertThat(data).hasSize(2); + @SuppressWarnings("unchecked") + Map mySecretData = (Map) result.data().get("my-secret"); + Assertions.assertThat(mySecretData.get("property")).isEqualTo("value"); + + @SuppressWarnings("unchecked") + Map mySecretData2 = (Map) result.data().get("my-secret-2"); + Assertions.assertThat(mySecretData2.get("property-2")).isEqualTo("value-2"); + } + + /** + *
+	 *     - secret deployed with name "blue-circle-secret" and labels "color=blue, shape=circle, tag=fit"
+	 *     - secret deployed with name "blue-square-secret" and labels "color=blue, shape=square, tag=fit"
+	 *     - secret deployed with name "blue-triangle-secret" and labels "color=blue, shape=triangle, tag=no-fit"
+	 *     - secret deployed with name "blue-square-secret-k8s" and labels "color=blue, shape=triangle, tag=no-fit"
+	 *
+	 *     - we search by labels "color=blue, tag=fits", as such find two secrets: "blue-circle-secret"
+	 *       and "blue-square-secret".
+	 * 
+ */ + @Test + void testSecretDataByLabelsThreeSecretsFound() { + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("blue-circle-secret") + .withLabels(Map.of("color", "blue", "shape", "circle", "tag", "fit")) + .build()) + .addToData(Map.of("one", Base64.getEncoder().encodeToString("1".getBytes()))) + .build()) + .create(); + + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("blue-square-secret") + .withLabels(Map.of("color", "blue", "shape", "square", "tag", "fit")) + .build()) + .addToData(Map.of("two", Base64.getEncoder().encodeToString("2".getBytes()))) + .build()) + .create(); + + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("blue-triangle-secret") + .withLabels(Map.of("color", "blue", "shape", "triangle", "tag", "no-fit")) + .build()) + .addToData(Map.of("three", Base64.getEncoder().encodeToString("3".getBytes()))) + .build()) + .create(); + + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("blue-square-secret-k8s") + .withLabels(Map.of("color", "blue", "shape", "triangle", "tag", "no-fit")) + .build()) + .addToData(Map.of("four", Base64.getEncoder().encodeToString("4".getBytes()))) + .build()) + .create(); + + MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByLabels(client, "spring-k8s", + Map.of("tag", "fit", "color", "blue"), new MockEnvironment(), NAMESPACED_BATCH_READ); + + Assertions.assertThat(result.names()).contains("blue-circle-secret"); + Assertions.assertThat(result.names()).contains("blue-square-secret"); + + Assertions.assertThat(result.data()).hasSize(2); + + @SuppressWarnings("unchecked") + Map dataOne = (Map) result.data().get("blue-circle-secret"); + Assertions.assertThat(dataOne).containsExactlyInAnyOrderEntriesOf(Map.of("one", "1")); + + @SuppressWarnings("unchecked") + Map dataTwo = (Map) result.data().get("blue-square-secret"); + Assertions.assertThat(dataTwo).containsExactlyInAnyOrderEntriesOf(Map.of("two", "2")); + + } + + /** + *
+	 * 		- secret 'my-secret' is deployed; we search for it by name and do not find it.
+	 * 
+ */ + @Test + void testSecretDataByNameSecretNotFound() { + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder().withMetadata(new ObjectMetaBuilder().withName("my-secret").build()).build()) + .create(); + LinkedHashSet names = new LinkedHashSet<>(); + names.add("nope"); + MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByName(client, "spring-k8s", names, + new MockEnvironment(), NAMESPACED_BATCH_READ); + Assertions.assertThat(result.names()).isEmpty(); + Assertions.assertThat(result.data()).isEmpty(); + } + + /** + *
+	 * 		- secret "my-secret" is deployed; we search for it by name and find it.
+	 * 
+ */ + @Test + void testSecretDataByNameSecretFound() { + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder().withMetadata(new ObjectMetaBuilder().withName("my-secret").build()) + .addToData(Map.of("property", Base64.getEncoder().encodeToString("value".getBytes()))) + .build()) + .create(); + LinkedHashSet names = new LinkedHashSet<>(); + names.add("my-secret"); + + MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByName(client, "spring-k8s", names, + new MockEnvironment(), NAMESPACED_BATCH_READ); + Assertions.assertThat(result.names()).hasSize(1); + + @SuppressWarnings("unchecked") + Map data = (Map) result.data().get("my-secret"); + Assertions.assertThat(data.get("property")).isEqualTo("value"); + } + + /** + *
+	 * 		- config-map "my-config-map" is deployed without any data
+	 * 		- we search for it by name and find it; but it has no data.
+	 * 
+ */ + @Test + void testConfigMapsDataByNameFoundNoData() { + client.configMaps() + .inNamespace("spring-k8s") + .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) + .build()) + .create(); + LinkedHashSet names = new LinkedHashSet<>(); + names.add("my-config-map"); + + MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsDataByName(client, "spring-k8s", names, + new MockEnvironment(), NAMESPACED_BATCH_READ); + Assertions.assertThat(result.names()).containsExactlyInAnyOrder("my-config-map"); + + @SuppressWarnings("unchecked") + Map data = (Map) result.data().get("my-config-map"); + Assertions.assertThat(data).isEmpty(); + } + + /** + *
+	 *     	- config-map "my-config-map" is deployed; we search for it and do not find it.
+	 * 
+ */ + @Test + void testConfigMapsDataByNameNotFound() { + client.configMaps() + .inNamespace("spring-k8s") + .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) + .build()) + .create(); + LinkedHashSet names = new LinkedHashSet<>(); + names.add("my-config-map-not-found"); + MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsDataByName(client, "spring-k8s", names, + new MockEnvironment(), NAMESPACED_BATCH_READ); + Assertions.assertThat(result.names()).isEmpty(); + Assertions.assertThat(result.data()).isEmpty(); + } + + /** + *
+	 *     - config-map "my-config-map" is deployed; we search for it and find it
+	 * 
+ */ + @Test + void testConfigMapDataByNameFound() { + client.configMaps() + .inNamespace("spring-k8s") + .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) + .addToData(Map.of("property", "value")) + .build()) + .create(); + + LinkedHashSet names = new LinkedHashSet<>(); + names.add("my-config-map"); + + MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsDataByName(client, "spring-k8s", names, + new MockEnvironment(), NAMESPACED_BATCH_READ); + Assertions.assertThat(result.names()).containsExactlyInAnyOrder("my-config-map"); + + @SuppressWarnings("unchecked") + Map data = (Map) result.data().get("my-config-map"); + Assertions.assertThat(data).containsExactlyInAnyOrderEntriesOf(Map.of("property", "value")); + } + + /** + *
+	 *     - config-map "my-config-map" is deployed
+	 *     - we search for it and find it
+	 *     - it contains a single .yaml property, as such it gets some special treatment
+	 * 
+ */ + @Test + void testConfigMapDataByNameFoundWithPropertyFile() { + client.configMaps() + .inNamespace("spring-k8s") + .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) + .addToData(Map.of(APPLICATION_YAML, "key1: value1")) + .build()) + .create(); + + LinkedHashSet names = new LinkedHashSet<>(); + names.add("my-config-map"); + + MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsDataByName(client, "spring-k8s", names, + new MockEnvironment(), NAMESPACED_BATCH_READ); + Assertions.assertThat(result.names()).containsExactlyInAnyOrder("my-config-map"); + + @SuppressWarnings("unchecked") + Map data = (Map) result.data().get("my-config-map"); + Assertions.assertThat(data).containsExactlyInAnyOrderEntriesOf(Map.of("key1", "value1")); + } + + /** + *
+	 *     - config-map "my-config-map" and "my-config-map-2" are deployed
+	 *     - we search and find them.
+	 * 
+ */ + @Test + void testConfigMapDataByNameTwoFound() { + client.configMaps() + .inNamespace("spring-k8s") + .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) + .addToData(Map.of("property", "value")) + .build()) + .create(); + + client.configMaps() + .inNamespace("spring-k8s") + .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map-2").build()) + .addToData(Map.of("property-2", "value-2")) + .build()) + .create(); + + LinkedHashSet names = new LinkedHashSet<>(); + names.add("my-config-map"); + names.add("my-config-map-2"); + + MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsDataByName(client, "spring-k8s", names, + new MockEnvironment(), NAMESPACED_BATCH_READ); + Assertions.assertThat(result.names()).contains("my-config-map"); + Assertions.assertThat(result.names()).contains("my-config-map-2"); + + Assertions.assertThat(result.data()).hasSize(2); + + @SuppressWarnings("unchecked") + Map data = (Map) result.data().get("my-config-map"); + Assertions.assertThat(data).contains(Map.entry("property", "value")); + + @SuppressWarnings("unchecked") + Map data2 = (Map) result.data().get("my-config-map-2"); + Assertions.assertThat(data2).contains(Map.entry("property-2", "value-2")); + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigUtilsNonNamespacedBatchReadTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigUtilsNonNamespacedBatchReadTests.java new file mode 100644 index 0000000000..b300ec4069 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigUtilsNonNamespacedBatchReadTests.java @@ -0,0 +1,423 @@ +/* + * Copyright 2013-2024 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.LinkedHashSet; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.kubernetes.commons.config.MultipleSourcesContainer; +import org.springframework.mock.env.MockEnvironment; + +import static org.springframework.cloud.kubernetes.commons.config.Constants.APPLICATION_YAML; + +/** + * @author wind57 + */ +@EnableKubernetesMockClient(crud = true, https = false) +class Fabric8ConfigUtilsNonNamespacedBatchReadTests { + + private static final boolean NAMESPACED_BATCH_READ = false; + + private KubernetesClient client; + + @AfterEach + void afterEach() { + new Fabric8SourcesNamespaceBatched().discardSecrets(); + new Fabric8SourcesNamespaceBatched().discardConfigMaps(); + } + + /** + *
+	 *  	- secret 'my-secret' is deployed without any labels
+	 *  	- we search for it by labels 'color=red' and do not find it.
+	 * 
+ */ + @Test + void testSecretDataByLabelsSecretNotFound() { + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder().withMetadata(new ObjectMetaBuilder().withName("my-secret").build()).build()) + .create(); + MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByLabels(client, "spring-k8s", + Map.of("color", "red"), new MockEnvironment(), NAMESPACED_BATCH_READ); + Assertions.assertThat(result.data()).isEmpty(); + Assertions.assertThat(result.names()).isEmpty(); + } + + /** + *
+	 *		- secret 'my-secret' is deployed with label '{color:pink}'
+	 *		- we search for it by same label and find it.
+	 * 
+ */ + @Test + void testSecretDataByLabelsSecretFound() { + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("my-secret").withLabels(Map.of("color", "pink")).build()) + .addToData(Map.of("property", Base64.getEncoder().encodeToString("value".getBytes()))) + .build()) + .create(); + + MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByLabels(client, "spring-k8s", + Map.of("color", "pink"), new MockEnvironment(), NAMESPACED_BATCH_READ); + Assertions.assertThat(result.names()).containsExactlyInAnyOrder("my-secret"); + + @SuppressWarnings("unchecked") + Map data = (Map) result.data().get("my-secret"); + Assertions.assertThat(data.get("property")).isEqualTo("value"); + + } + + /** + *
+	 * 		- secret 'my-secret' is deployed with label '{color:pink}'
+	 * 		- we search for it by same label and find it.
+	 * 		- This secret contains a single .yaml property, as such, it gets some special treatment.
+	 * 
+ */ + @Test + void testSecretDataByLabelsSecretFoundWithPropertyFile() { + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("my-secret").withLabels(Map.of("color", "pink")).build()) + .addToData(Map.of(APPLICATION_YAML, Base64.getEncoder().encodeToString("key1: value1".getBytes()))) + .build()) + .create(); + + MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByLabels(client, "spring-k8s", + Map.of("color", "pink"), new MockEnvironment(), NAMESPACED_BATCH_READ); + Assertions.assertThat(result.names()).containsExactlyInAnyOrder("my-secret"); + + @SuppressWarnings("unchecked") + Map data = (Map) result.data().get("my-secret"); + Assertions.assertThat(data.get("key1")).isEqualTo("value1"); + + } + + /** + *
+	 * 		- secrets 'my-secret' and 'my-secret-2' are deployed with label {color:pink}
+	 * 		- we search for them by same label and find them.
+	 * 
+ */ + @Test + void testSecretDataByLabelsTwoSecretsFound() { + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("my-secret").withLabels(Map.of("color", "pink")).build()) + .addToData(Map.of("property", Base64.getEncoder().encodeToString("value".getBytes()))) + .build()) + .create(); + + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata( + new ObjectMetaBuilder().withName("my-secret-2").withLabels(Map.of("color", "pink")).build()) + .addToData(Map.of("property-2", Base64.getEncoder().encodeToString("value-2".getBytes()))) + .build()) + .create(); + + MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByLabels(client, "spring-k8s", + Map.of("color", "pink"), new MockEnvironment(), NAMESPACED_BATCH_READ); + Assertions.assertThat(result.names()).contains("my-secret"); + Assertions.assertThat(result.names()).contains("my-secret-2"); + + Assertions.assertThat(result.data()).hasSize(2); + + @SuppressWarnings("unchecked") + Map secretData = (Map) result.data().get("my-secret"); + Assertions.assertThat(secretData.get("property")).isEqualTo("value"); + + @SuppressWarnings("unchecked") + Map secretData2 = (Map) result.data().get("my-secret-2"); + Assertions.assertThat(secretData2.get("property-2")).isEqualTo("value-2"); + + } + + /** + *
+	 *     - secret deployed with name "blue-circle-secret" and labels "color=blue, shape=circle, tag=fit"
+	 *     - secret deployed with name "blue-square-secret" and labels "color=blue, shape=square, tag=fit"
+	 *     - secret deployed with name "blue-triangle-secret" and labels "color=blue, shape=triangle, tag=no-fit"
+	 *     - secret deployed with name "blue-square-secret-k8s" and labels "color=blue, shape=triangle, tag=no-fit"
+	 *
+	 *     - we search by labels "color=blue, tag=fits", as such find two secrets: "blue-circle-secret"
+	 *       and "blue-square-secret".
+	 * 
+ */ + @Test + void testSecretDataByLabelsThreeSecretsFound() { + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("blue-circle-secret") + .withLabels(Map.of("color", "blue", "shape", "circle", "tag", "fit")) + .build()) + .addToData(Map.of("one", Base64.getEncoder().encodeToString("1".getBytes()))) + .build()) + .create(); + + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("blue-square-secret") + .withLabels(Map.of("color", "blue", "shape", "square", "tag", "fit")) + .build()) + .addToData(Map.of("two", Base64.getEncoder().encodeToString("2".getBytes()))) + .build()) + .create(); + + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("blue-triangle-secret") + .withLabels(Map.of("color", "blue", "shape", "triangle", "tag", "no-fit")) + .build()) + .addToData(Map.of("three", Base64.getEncoder().encodeToString("3".getBytes()))) + .build()) + .create(); + + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("blue-square-secret-k8s") + .withLabels(Map.of("color", "blue", "shape", "triangle", "tag", "no-fit")) + .build()) + .addToData(Map.of("four", Base64.getEncoder().encodeToString("4".getBytes()))) + .build()) + .create(); + + MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByLabels(client, "spring-k8s", + Map.of("tag", "fit", "color", "blue"), new MockEnvironment(), NAMESPACED_BATCH_READ); + + Assertions.assertThat(result.names()).contains("blue-circle-secret"); + Assertions.assertThat(result.names()).contains("blue-square-secret"); + + @SuppressWarnings("unchecked") + Map data = (Map) result.data().get("blue-circle-secret"); + Assertions.assertThat(data.get("one")).isEqualTo("1"); + + @SuppressWarnings("unchecked") + Map data2 = (Map) result.data().get("blue-square-secret"); + Assertions.assertThat(data2.get("two")).isEqualTo("2"); + + } + + /** + *
+	 * 		- secret 'my-secret' is deployed; we search for it by name and do not find it.
+	 * 
+ */ + @Test + void testSecretDataByNameSecretNotFound() { + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder().withMetadata(new ObjectMetaBuilder().withName("my-secret").build()).build()) + .create(); + LinkedHashSet names = new LinkedHashSet<>(); + names.add("nope"); + MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByName(client, "spring-k8s", names, + new MockEnvironment(), NAMESPACED_BATCH_READ); + Assertions.assertThat(result.names()).isEmpty(); + Assertions.assertThat(result.data()).isEmpty(); + } + + /** + *
+	 * 		- secret "my-secret" is deployed; we search for it by name and find it.
+	 * 
+ */ + @Test + void testSecretDataByNameSecretFound() { + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder().withMetadata(new ObjectMetaBuilder().withName("my-secret").build()) + .addToData(Map.of("property", Base64.getEncoder().encodeToString("value".getBytes()))) + .build()) + .create(); + LinkedHashSet names = new LinkedHashSet<>(); + names.add("my-secret"); + + MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByName(client, "spring-k8s", names, + new MockEnvironment(), NAMESPACED_BATCH_READ); + Assertions.assertThat(result.names()).hasSize(1); + + @SuppressWarnings("unchecked") + Map data = (Map) result.data().get("my-secret"); + Assertions.assertThat(data.get("property")).isEqualTo("value"); + } + + /** + *
+	 * 		- config-map "my-config-map" is deployed without any data
+	 * 		- we search for it by name and find it; but it has no data.
+	 * 
+ */ + @Test + void testConfigMapsDataByNameFoundNoData() { + client.configMaps() + .inNamespace("spring-k8s") + .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) + .build()) + .create(); + LinkedHashSet names = new LinkedHashSet<>(); + names.add("my-config-map"); + + MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsDataByName(client, "spring-k8s", names, + new MockEnvironment(), NAMESPACED_BATCH_READ); + Assertions.assertThat(result.names()).containsExactlyInAnyOrder("my-config-map"); + + @SuppressWarnings("unchecked") + Map data = (Map) result.data().get("my-config-map"); + Assertions.assertThat(data).isEmpty(); + } + + /** + *
+	 *     	- config-map "my-config-map" is deployed; we search for it and do not find it.
+	 * 
+ */ + @Test + void testConfigMapsDataByNameNotFound() { + client.configMaps() + .inNamespace("spring-k8s") + .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) + .build()) + .create(); + LinkedHashSet names = new LinkedHashSet<>(); + names.add("my-config-map-not-found"); + MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsDataByName(client, "spring-k8s", names, + new MockEnvironment(), NAMESPACED_BATCH_READ); + Assertions.assertThat(result.names()).isEmpty(); + Assertions.assertThat(result.data()).isEmpty(); + } + + /** + *
+	 *     - config-map "my-config-map" is deployed; we search for it and find it
+	 * 
+ */ + @Test + void testConfigMapDataByNameFound() { + client.configMaps() + .inNamespace("spring-k8s") + .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) + .addToData(Map.of("property", "value")) + .build()) + .create(); + + LinkedHashSet names = new LinkedHashSet<>(); + names.add("my-config-map"); + + MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsDataByName(client, "spring-k8s", names, + new MockEnvironment(), NAMESPACED_BATCH_READ); + Assertions.assertThat(result.names()).containsExactlyInAnyOrder("my-config-map"); + + @SuppressWarnings("unchecked") + Map data = (Map) result.data().get("my-config-map"); + Assertions.assertThat(data.get("property")).isEqualTo("value"); + + } + + /** + *
+	 *     - config-map "my-config-map" is deployed
+	 *     - we search for it and find it
+	 *     - it contains a single .yaml property, as such it gets some special treatment
+	 * 
+ */ + @Test + void testConfigMapDataByNameFoundWithPropertyFile() { + client.configMaps() + .inNamespace("spring-k8s") + .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) + .addToData(Map.of(APPLICATION_YAML, "key1: value1")) + .build()) + .create(); + + LinkedHashSet names = new LinkedHashSet<>(); + names.add("my-config-map"); + + MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsDataByName(client, "spring-k8s", names, + new MockEnvironment(), NAMESPACED_BATCH_READ); + Assertions.assertThat(result.names()).containsExactlyInAnyOrder("my-config-map"); + + @SuppressWarnings("unchecked") + Map data = (Map) result.data().get("my-config-map"); + Assertions.assertThat(data.get("key1")).isEqualTo("value1"); + + } + + /** + *
+	 *     - config-map "my-config-map" and "my-config-map-2" are deployed
+	 *     - we search and find them.
+	 * 
+ */ + @Test + void testConfigMapDataByNameTwoFound() { + client.configMaps() + .inNamespace("spring-k8s") + .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) + .addToData(Map.of("property", "value")) + .build()) + .create(); + + client.configMaps() + .inNamespace("spring-k8s") + .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map-2").build()) + .addToData(Map.of("property-2", "value-2")) + .build()) + .create(); + + LinkedHashSet names = new LinkedHashSet<>(); + names.add("my-config-map"); + names.add("my-config-map-2"); + + MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsDataByName(client, "spring-k8s", names, + new MockEnvironment(), NAMESPACED_BATCH_READ); + Assertions.assertThat(result.names()).contains("my-config-map"); + Assertions.assertThat(result.names()).contains("my-config-map-2"); + + Assertions.assertThat(result.data()).hasSize(2); + + @SuppressWarnings("unchecked") + Map data = (Map) result.data().get("my-config-map"); + Assertions.assertThat(data.get("property")).isEqualTo("value"); + + @SuppressWarnings("unchecked") + Map data2 = (Map) result.data().get("my-config-map-2"); + Assertions.assertThat(data2.get("property-2")).isEqualTo("value-2"); + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigUtilsTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigUtilsTests.java index 4be4ed2ea3..85cb6f570c 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigUtilsTests.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2022 the original author or authors. + * Copyright 2013-2024 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. @@ -17,402 +17,22 @@ package org.springframework.cloud.kubernetes.fabric8.config; import java.time.Duration; -import java.util.Base64; -import java.util.LinkedHashSet; -import java.util.Map; import java.util.Set; -import io.fabric8.kubernetes.api.model.ConfigMapBuilder; -import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.fabric8.kubernetes.api.model.SecretBuilder; -import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; -import org.springframework.cloud.kubernetes.commons.config.MultipleSourcesContainer; import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; import org.springframework.mock.env.MockEnvironment; -import static org.springframework.cloud.kubernetes.commons.config.Constants.APPLICATION_YAML; - /** * @author wind57 */ @EnableKubernetesMockClient(crud = true, https = false) class Fabric8ConfigUtilsTests { - private KubernetesClient client; - - @AfterEach - void afterEach() { - new Fabric8ConfigMapsCache().discardAll(); - new Fabric8SecretsCache().discardAll(); - } - - // secret "my-secret" is deployed without any labels; we search for it by labels - // "color=red" and do not find it. - @Test - void testSecretDataByLabelsSecretNotFound() { - client.secrets() - .inNamespace("spring-k8s") - .resource(new SecretBuilder().withMetadata(new ObjectMetaBuilder().withName("my-secret").build()).build()) - .create(); - MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByLabels(client, "spring-k8s", - Map.of("color", "red"), new MockEnvironment(), Set.of()); - Assertions.assertThat(result.data()).isEmpty(); - Assertions.assertThat(result.names()).isEmpty(); - } - - // secret "my-secret" is deployed with label {color:pink}; we search for it by same - // label and find it. - @Test - void testSecretDataByLabelsSecretFound() { - client.secrets() - .inNamespace("spring-k8s") - .resource(new SecretBuilder() - .withMetadata(new ObjectMetaBuilder().withName("my-secret").withLabels(Map.of("color", "pink")).build()) - .addToData(Map.of("property", Base64.getEncoder().encodeToString("value".getBytes()))) - .build()) - .create(); - - MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByLabels(client, "spring-k8s", - Map.of("color", "pink"), new MockEnvironment(), Set.of()); - Assertions.assertThat(result.names()).containsExactlyInAnyOrder("my-secret"); - - @SuppressWarnings("unchecked") - Map data = (Map) result.data().get("my-secret"); - Assertions.assertThat(data).containsExactlyInAnyOrderEntriesOf(Map.of("property", "value")); - } - - // secret "my-secret" is deployed with label {color:pink}; we search for it by same - // label and find it. This secret contains a single .yaml property, as such - // it gets some special treatment. - @Test - void testSecretDataByLabelsSecretFoundWithPropertyFile() { - client.secrets() - .inNamespace("spring-k8s") - .resource(new SecretBuilder() - .withMetadata(new ObjectMetaBuilder().withName("my-secret").withLabels(Map.of("color", "pink")).build()) - .addToData(Map.of(APPLICATION_YAML, Base64.getEncoder().encodeToString("key1: value1".getBytes()))) - .build()) - .create(); - - MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByLabels(client, "spring-k8s", - Map.of("color", "pink"), new MockEnvironment(), Set.of()); - Assertions.assertThat(result.names()).containsExactlyInAnyOrder("my-secret"); - - @SuppressWarnings("unchecked") - Map data = (Map) result.data().get("my-secret"); - Assertions.assertThat(data).containsExactlyInAnyOrderEntriesOf(Map.of("key1", "value1")); - } - - // secrets "my-secret" and "my-secret-2" are deployed with label {color:pink}; - // we search for them by same label and find them. - @Test - void testSecretDataByLabelsTwoSecretsFound() { - client.secrets() - .inNamespace("spring-k8s") - .resource(new SecretBuilder() - .withMetadata(new ObjectMetaBuilder().withName("my-secret").withLabels(Map.of("color", "pink")).build()) - .addToData(Map.of("property", Base64.getEncoder().encodeToString("value".getBytes()))) - .build()) - .create(); - - client.secrets() - .inNamespace("spring-k8s") - .resource(new SecretBuilder() - .withMetadata( - new ObjectMetaBuilder().withName("my-secret-2").withLabels(Map.of("color", "pink")).build()) - .addToData(Map.of("property-2", Base64.getEncoder().encodeToString("value-2".getBytes()))) - .build()) - .create(); - - MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByLabels(client, "spring-k8s", - Map.of("color", "pink"), new MockEnvironment(), Set.of()); - Assertions.assertThat(result.names()).contains("my-secret"); - Assertions.assertThat(result.names()).contains("my-secret-2"); - - @SuppressWarnings("unchecked") - Map mySecretData = (Map) result.data().get("my-secret"); - Assertions.assertThat(mySecretData).containsExactlyInAnyOrderEntriesOf(Map.of("property", "value")); - - @SuppressWarnings("unchecked") - Map mySecret2Data = (Map) result.data().get("my-secret-2"); - Assertions.assertThat(mySecret2Data).containsExactlyInAnyOrderEntriesOf(Map.of("property-2", "value-2")); - } - - /** - *
-	 *     - secret deployed with name "blue-circle-secret" and labels "color=blue, shape=circle, tag=fit"
-	 *     - secret deployed with name "blue-square-secret" and labels "color=blue, shape=square, tag=fit"
-	 *     - secret deployed with name "blue-triangle-secret" and labels "color=blue, shape=triangle, tag=no-fit"
-	 *     - secret deployed with name "blue-square-secret-k8s" and labels "color=blue, shape=triangle, tag=no-fit"
-	 *
-	 *     - we search by labels "color=blue, tag=fits", as such first find two secrets: "blue-circle-secret"
-	 *       and "blue-square-secret".
-	 *     - since "k8s" profile is enabled, we also take "blue-square-secret-k8s". Notice that this one does not match
-	 *       the initial labels (it has "tag=no-fit"), but it does not matter, we take it anyway.
-	 * 
- */ - @Test - void testSecretDataByLabelsThreeSecretsFound() { - client.secrets() - .inNamespace("spring-k8s") - .resource(new SecretBuilder() - .withMetadata(new ObjectMetaBuilder().withName("blue-circle-secret") - .withLabels(Map.of("color", "blue", "shape", "circle", "tag", "fit")) - .build()) - .addToData(Map.of("one", Base64.getEncoder().encodeToString("1".getBytes()))) - .build()) - .create(); - - client.secrets() - .inNamespace("spring-k8s") - .resource(new SecretBuilder() - .withMetadata(new ObjectMetaBuilder().withName("blue-square-secret") - .withLabels(Map.of("color", "blue", "shape", "square", "tag", "fit")) - .build()) - .addToData(Map.of("two", Base64.getEncoder().encodeToString("2".getBytes()))) - .build()) - .create(); - - client.secrets() - .inNamespace("spring-k8s") - .resource(new SecretBuilder() - .withMetadata(new ObjectMetaBuilder().withName("blue-triangle-secret") - .withLabels(Map.of("color", "blue", "shape", "triangle", "tag", "no-fit")) - .build()) - .addToData(Map.of("three", Base64.getEncoder().encodeToString("3".getBytes()))) - .build()) - .create(); - - client.secrets() - .inNamespace("spring-k8s") - .resource(new SecretBuilder() - .withMetadata(new ObjectMetaBuilder().withName("blue-square-secret-k8s") - .withLabels(Map.of("color", "blue", "shape", "triangle", "tag", "no-fit")) - .build()) - .addToData(Map.of("four", Base64.getEncoder().encodeToString("4".getBytes()))) - .build()) - .create(); - - MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByLabels(client, "spring-k8s", - Map.of("tag", "fit", "color", "blue"), new MockEnvironment(), Set.of("k8s")); - - Assertions.assertThat(result.names()).contains("blue-circle-secret"); - Assertions.assertThat(result.names()).contains("blue-square-secret"); - Assertions.assertThat(result.names()).contains("blue-square-secret-k8s"); - - @SuppressWarnings("unchecked") - Map dataBlueSecret = (Map) result.data().get("blue-circle-secret"); - Assertions.assertThat(dataBlueSecret).containsExactlyInAnyOrderEntriesOf(Map.of("one", "1")); - - @SuppressWarnings("unchecked") - Map dataSquareSecret = (Map) result.data().get("blue-square-secret"); - Assertions.assertThat(dataSquareSecret).containsExactlyInAnyOrderEntriesOf(Map.of("two", "2")); - - @SuppressWarnings("unchecked") - Map dataSquareSecretK8s = (Map) result.data().get("blue-square-secret-k8s"); - Assertions.assertThat(dataSquareSecretK8s).containsExactlyInAnyOrderEntriesOf(Map.of("four", "4")); - - } - - // secret "my-secret" is deployed; we search for it by name and do not find it. - @Test - void testSecretDataByNameSecretNotFound() { - client.secrets() - .inNamespace("spring-k8s") - .resource(new SecretBuilder().withMetadata(new ObjectMetaBuilder().withName("my-secret").build()).build()) - .create(); - LinkedHashSet names = new LinkedHashSet<>(); - names.add("nope"); - MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByName(client, "spring-k8s", names, - new MockEnvironment()); - Assertions.assertThat(result.names()).isEmpty(); - Assertions.assertThat(result.data()).isEmpty(); - } - - // secret "my-secret" is deployed; we search for it by name and find it. - @Test - void testSecretDataByNameSecretFound() { - client.secrets() - .inNamespace("spring-k8s") - .resource(new SecretBuilder().withMetadata(new ObjectMetaBuilder().withName("my-secret").build()) - .addToData(Map.of("property", Base64.getEncoder().encodeToString("value".getBytes()))) - .build()) - .create(); - LinkedHashSet names = new LinkedHashSet<>(); - names.add("my-secret"); - - MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByName(client, "spring-k8s", names, - new MockEnvironment()); - Assertions.assertThat(result.names().size()).isEqualTo(1); - - @SuppressWarnings("unchecked") - Map data = (Map) result.data().get("my-secret"); - Assertions.assertThat(data.get("property")).isEqualTo("value"); - } - - // secrets "my-secret" and "my-secret-2" are deployed; - // we search for them by name label and find them. - @Test - void testSecretDataByNameTwoSecretsFound() { - client.secrets() - .inNamespace("spring-k8s") - .resource(new SecretBuilder().withMetadata(new ObjectMetaBuilder().withName("my-secret").build()) - .addToData(Map.of("property", Base64.getEncoder().encodeToString("value".getBytes()))) - .build()) - .create(); - - client.secrets() - .inNamespace("spring-k8s") - .resource(new SecretBuilder().withMetadata(new ObjectMetaBuilder().withName("my-secret-2").build()) - .addToData(Map.of("property-2", Base64.getEncoder().encodeToString("value-2".getBytes()))) - .build()) - .create(); - LinkedHashSet names = new LinkedHashSet<>(); - names.add("my-secret"); - names.add("my-secret-2"); - - MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByName(client, "spring-k8s", names, - new MockEnvironment()); - Assertions.assertThat(result.names()).contains("my-secret"); - Assertions.assertThat(result.names()).contains("my-secret-2"); - - Assertions.assertThat(result.data().size()).isEqualTo(2); - - @SuppressWarnings("unchecked") - Map data = (Map) result.data().get("my-secret"); - Assertions.assertThat(data.get("property")).isEqualTo("value"); - - @SuppressWarnings("unchecked") - Map data2 = (Map) result.data().get("my-secret-2"); - Assertions.assertThat(data2.get("property-2")).isEqualTo("value-2"); - } - - // config-map "my-config-map" is deployed without any data; we search for it by name - // and find it; but it has no data. - @Test - void testConfigMapsDataByNameFoundNoData() { - client.configMaps() - .inNamespace("spring-k8s") - .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) - .build()) - .create(); - LinkedHashSet names = new LinkedHashSet<>(); - names.add("my-config-map"); - - MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsDataByName(client, "spring-k8s", names, - new MockEnvironment()); - Assertions.assertThat(result.names()).containsExactlyInAnyOrder("my-config-map"); - - @SuppressWarnings("unchecked") - Map data = (Map) result.data().get("my-config-map"); - Assertions.assertThat(data).isEmpty(); - } - - // config-map "my-config-map" is deployed; we search for it and do not find it. - @Test - void testConfigMapsDataByNameNotFound() { - client.configMaps() - .inNamespace("spring-k8s") - .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) - .build()) - .create(); - LinkedHashSet names = new LinkedHashSet<>(); - names.add("my-config-map-not-found"); - MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsDataByName(client, "spring-k8s", names, - new MockEnvironment()); - Assertions.assertThat(result.names()).isEmpty(); - Assertions.assertThat(result.data()).isEmpty(); - } - - // config-map "my-config-map" is deployed; we search for it and find it - @Test - void testConfigMapDataByNameFound() { - client.configMaps() - .inNamespace("spring-k8s") - .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) - .addToData(Map.of("property", "value")) - .build()) - .create(); - - LinkedHashSet names = new LinkedHashSet<>(); - names.add("my-config-map"); - - MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsDataByName(client, "spring-k8s", names, - new MockEnvironment()); - Assertions.assertThat(result.names()).containsExactlyInAnyOrder("my-config-map"); - - @SuppressWarnings("unchecked") - Map data = (Map) result.data().get("my-config-map"); - Assertions.assertThat(data).containsExactlyInAnyOrderEntriesOf(Map.of("property", "value")); - } - - // config-map "my-config-map" is deployed; we search for it and find it. - // It contains a single .yaml property, as such it gets some special treatment. - @Test - void testConfigMapDataByNameFoundWithPropertyFile() { - client.configMaps() - .inNamespace("spring-k8s") - .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) - .addToData(Map.of(APPLICATION_YAML, "key1: value1")) - .build()) - .create(); - - LinkedHashSet names = new LinkedHashSet<>(); - names.add("my-config-map"); - - MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsDataByName(client, "spring-k8s", names, - new MockEnvironment()); - Assertions.assertThat(result.names()).containsExactlyInAnyOrder("my-config-map"); - - @SuppressWarnings("unchecked") - Map data = (Map) result.data().get("my-config-map"); - Assertions.assertThat(data).containsExactlyInAnyOrderEntriesOf(Map.of("key1", "value1")); - } - - // config-map "my-config-map" and "my-config-map-2" are deployed; - // we search and find them. - @Test - void testConfigMapDataByNameTwoFound() { - client.configMaps() - .inNamespace("spring-k8s") - .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) - .addToData(Map.of("property", "value")) - .build()) - .create(); - - client.configMaps() - .inNamespace("spring-k8s") - .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map-2").build()) - .addToData(Map.of("property-2", "value-2")) - .build()) - .create(); - - LinkedHashSet names = new LinkedHashSet<>(); - names.add("my-config-map"); - names.add("my-config-map-2"); - - MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsDataByName(client, "spring-k8s", names, - new MockEnvironment()); - Assertions.assertThat(result.names()).contains("my-config-map"); - Assertions.assertThat(result.names()).contains("my-config-map-2"); - - Assertions.assertThat(result.data().size()).isEqualTo(2); - - @SuppressWarnings("unchecked") - Map data = (Map) result.data().get("my-config-map"); - Assertions.assertThat(data).containsExactlyInAnyOrderEntriesOf(Map.of("property", "value")); - - @SuppressWarnings("unchecked") - Map data2 = (Map) result.data().get("my-config-map-2"); - Assertions.assertThat(data2).containsExactlyInAnyOrderEntriesOf(Map.of("property-2", "value-2")); - } - @Test void testNamespacesFromProperties() { ConfigReloadProperties configReloadProperties = new ConfigReloadProperties(false, true, false, diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretErrorOnReadingSourceTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretErrorOnReadingSourceTests.java index 00bee4b51b..cc33a4649c 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretErrorOnReadingSourceTests.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretErrorOnReadingSourceTests.java @@ -48,6 +48,8 @@ @ExtendWith(OutputCaptureExtension.class) class Fabric8SecretErrorOnReadingSourceTests { + private static final boolean NAMESPACED_BATCHED = true; + private static KubernetesMockServer mockServer; private static KubernetesClient mockClient; @@ -71,7 +73,7 @@ void namedSingleSecretFails(CapturedOutput output) { mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once(); SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), - List.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT); + List.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT, NAMESPACED_BATCHED); Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient, secretsConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -105,7 +107,8 @@ void namedTwoSecretsOneFails(CapturedOutput output) { Source sourceTwo = new Source(secretNameTwo, namespace, Map.of(), null, null, null); SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), - List.of(sourceOne, sourceTwo), true, null, namespace, false, true, false, RetryProperties.DEFAULT); + List.of(sourceOne, sourceTwo), true, null, namespace, false, true, false, RetryProperties.DEFAULT, + NAMESPACED_BATCHED); Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient, secretsConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -140,7 +143,8 @@ void namedTwoSecretsBothFail(CapturedOutput output) { Source sourceTwo = new Source(secretNameTwo, namespace, Map.of(), null, null, null); SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), - List.of(sourceOne, sourceTwo), true, null, namespace, false, true, false, RetryProperties.DEFAULT); + List.of(sourceOne, sourceTwo), true, null, namespace, false, true, false, RetryProperties.DEFAULT, + NAMESPACED_BATCHED); Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient, secretsConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -171,7 +175,8 @@ void labeledSingleSecretFails(CapturedOutput output) { Source secretSource = new Source(null, namespace, labels, null, null, null); SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, labels, List.of(), - List.of(secretSource), true, null, namespace, false, true, false, RetryProperties.DEFAULT); + List.of(secretSource), true, null, namespace, false, true, false, RetryProperties.DEFAULT, + NAMESPACED_BATCHED); Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient, secretsConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -216,7 +221,7 @@ void labeledTwoSecretsOneFails(CapturedOutput output) { SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of("one", "1", "two", "2"), List.of(), List.of(sourceOne, sourceTwo), true, null, namespace, false, - true, false, RetryProperties.DEFAULT); + true, false, RetryProperties.DEFAULT, NAMESPACED_BATCHED); Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient, secretsConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -256,7 +261,7 @@ void labeledTwoConfigMapsBothFail(CapturedOutput output) { SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of("one", "1", "two", "2"), List.of(), List.of(sourceOne, sourceTwo), true, null, namespace, false, - true, false, RetryProperties.DEFAULT); + true, false, RetryProperties.DEFAULT, NAMESPACED_BATCHED); Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient, secretsConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); 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 e8ac439dc3..ca24c0c679 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 @@ -65,7 +65,7 @@ void locateShouldThrowExceptionOnFailureWhenFailFastIsEnabled() { mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").always(); SecretsConfigProperties configMapConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), - List.of(), true, name, namespace, false, true, true, RetryProperties.DEFAULT); + List.of(), true, name, namespace, false, true, true, RetryProperties.DEFAULT, true); Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -83,7 +83,7 @@ void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled(CapturedOutput mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").always(); SecretsConfigProperties configMapConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), - List.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT); + List.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT, true); Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); 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 a0a246eded..32f196e6ea 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 @@ -56,7 +56,7 @@ void namedStrategyShouldThrowExceptionOnFailureWhenFailFastIsEnabled() { final String path = String.format("/api/v1/namespaces/%s/secrets", namespace); NamedSecretNormalizedSource named = new NamedSecretNormalizedSource(name, namespace, true, false); - Fabric8ConfigContext context = new Fabric8ConfigContext(client, named, namespace, new MockEnvironment()); + Fabric8ConfigContext context = new Fabric8ConfigContext(client, named, namespace, new MockEnvironment(), true); mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").always(); assertThatThrownBy(() -> new Fabric8SecretsPropertySource(context)).isInstanceOf(IllegalStateException.class) @@ -70,8 +70,9 @@ void labeledStrategyShouldThrowExceptionOnFailureWhenFailFastIsEnabled() { final Map labels = Collections.singletonMap("a", "b"); final String path = String.format("/api/v1/namespaces/%s/secrets", namespace); - LabeledSecretNormalizedSource labeled = new LabeledSecretNormalizedSource(namespace, labels, true, false); - Fabric8ConfigContext context = new Fabric8ConfigContext(client, labeled, "default", new MockEnvironment()); + LabeledSecretNormalizedSource labeled = new LabeledSecretNormalizedSource(namespace, labels, true); + Fabric8ConfigContext context = new Fabric8ConfigContext(client, labeled, "default", new MockEnvironment(), + true); mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").always(); assertThatThrownBy(() -> new Fabric8SecretsPropertySource(context)).isInstanceOf(IllegalStateException.class) @@ -85,7 +86,7 @@ void namedStrategyShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() { final String path = String.format("/api/v1/namespaces/%s/secrets", namespace); NamedSecretNormalizedSource named = new NamedSecretNormalizedSource(name, namespace, false, false); - Fabric8ConfigContext context = new Fabric8ConfigContext(client, named, "default", new MockEnvironment()); + Fabric8ConfigContext context = new Fabric8ConfigContext(client, named, "default", new MockEnvironment(), true); mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").always(); assertThatNoException().isThrownBy(() -> new Fabric8SecretsPropertySource(context)); @@ -97,8 +98,9 @@ void labeledStrategyShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() { final Map labels = Collections.singletonMap("a", "b"); final String path = String.format("/api/v1/namespaces/%s/secrets", namespace); - LabeledSecretNormalizedSource labeled = new LabeledSecretNormalizedSource(namespace, labels, false, false); - Fabric8ConfigContext context = new Fabric8ConfigContext(client, labeled, "default", new MockEnvironment()); + LabeledSecretNormalizedSource labeled = new LabeledSecretNormalizedSource(namespace, labels, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(client, labeled, "default", new MockEnvironment(), + true); mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").always(); assertThatNoException().isThrownBy(() -> new Fabric8SecretsPropertySource(context)); diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledConfigMapContextToSourceDataProviderTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledConfigMapContextToSourceDataProviderNamespacedBatchReadTests.java similarity index 93% rename from spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledConfigMapContextToSourceDataProviderTests.java rename to spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledConfigMapContextToSourceDataProviderNamespacedBatchReadTests.java index 88c8e93dc8..cef325861e 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledConfigMapContextToSourceDataProviderTests.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledConfigMapContextToSourceDataProviderNamespacedBatchReadTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2024 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. @@ -47,7 +47,9 @@ */ @EnableKubernetesMockClient(crud = true, https = false) @ExtendWith(OutputCaptureExtension.class) -class LabeledConfigMapContextToSourceDataProviderTests { +class LabeledConfigMapContextToSourceDataProviderNamespacedBatchReadTests { + + private static final boolean NAMESPACED_BATCH_READ = true; private static final String NAMESPACE = "default"; @@ -82,7 +84,7 @@ static void beforeAll() { @AfterEach void afterEach() { mockClient.configMaps().inNamespace(NAMESPACE).delete(); - new Fabric8ConfigMapsCache().discardAll(); + new Fabric8SourcesNamespaceBatched().discardConfigMaps(); } /** @@ -103,7 +105,7 @@ void singleConfigMapMatchAgainstLabels() { NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, LABELS, true, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -147,7 +149,7 @@ void twoConfigMapsMatchAgainstLabels() { NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, RED_LABEL, true, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -176,7 +178,7 @@ void configMapNoMatch() { NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, BLUE_LABEL, true, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -207,7 +209,7 @@ void namespaceMatch() { NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE + "nope", LABELS, true, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -237,7 +239,7 @@ void testWithPrefix() { NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, Collections.singletonMap("color", "blue"), true, mePrefix, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -279,7 +281,7 @@ void testTwoConfigmapsWithPrefix() { NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DELAYED, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -305,8 +307,7 @@ void testTwoConfigmapsWithPrefix() { /** * two configmaps are deployed: "color-configmap" with label: "{color:blue}" and * "color-configmap-k8s" with no labels. We search by "{color:red}", do not find - * anything and thus have an empty SourceData. profile based sources are enabled, but - * it has no effect. + * anything and thus have an empty SourceData. */ @Test void searchWithLabelsNoConfigmapsFound() { @@ -326,11 +327,11 @@ void searchWithLabelsNoConfigmapsFound() { mockClient.configMaps().inNamespace(NAMESPACE).resource(colorConfigmap).create(); mockClient.configMaps().inNamespace(NAMESPACE).resource(colorConfigmapK8s).create(); MockEnvironment environment = new MockEnvironment(); - environment.setActiveProfiles("k8s"); NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, Collections.singletonMap("color", "red"), true, ConfigUtils.Prefix.DEFAULT, true); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -343,7 +344,7 @@ void searchWithLabelsNoConfigmapsFound() { /** * two configmaps are deployed: "color-configmap" with label: "{color:blue}" and * "shape-configmap" with label: "{shape:round}". We search by "{color:blue}" and find - * one configmap. profile based sources are enabled, but it has no effect. + * one configmap. */ @Test void searchWithLabelsOneConfigMapFound() { @@ -363,11 +364,11 @@ void searchWithLabelsOneConfigMapFound() { mockClient.configMaps().inNamespace(NAMESPACE).resource(colorConfigmap).create(); mockClient.configMaps().inNamespace(NAMESPACE).resource(shapeConfigmap).create(); MockEnvironment environment = new MockEnvironment(); - environment.setActiveProfiles("k8s"); NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DEFAULT, true); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -407,15 +408,15 @@ void searchWithLabelsOneConfigMapFoundAndOneFromProfileFound() { NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DELAYED, true); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + true); Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); - assertThat(sourceData.sourceData().size()).isEqualTo(2); + assertThat(sourceData.sourceData().size()).isEqualTo(1); assertThat(sourceData.sourceData().get("color-configmap.one")).isEqualTo("1"); - assertThat(sourceData.sourceData().get("color-configmap-k8s.two")).isEqualTo("2"); - assertThat(sourceData.sourceName()).isEqualTo("configmap.color-configmap.color-configmap-k8s.default"); + assertThat(sourceData.sourceName()).isEqualTo("configmap.color-configmap.default"); } @@ -429,7 +430,7 @@ void searchWithLabelsOneConfigMapFoundAndOneFromProfileFound() { * */ @Test - void searchWithLabelsTwoConfigMapsFoundAndOneFromProfileFound() { + void searchWithLabelsTwoConfigMapsFound() { ConfigMap colorConfigMap = new ConfigMapBuilder().withNewMetadata() .withName("color-configmap") .withLabels(Collections.singletonMap("color", "blue")) @@ -472,24 +473,21 @@ void searchWithLabelsTwoConfigMapsFoundAndOneFromProfileFound() { mockClient.configMaps().inNamespace(NAMESPACE).resource(shapeConfigmapK8s).create(); MockEnvironment environment = new MockEnvironment(); - environment.setActiveProfiles("k8s"); NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DELAYED, true); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); - assertThat(sourceData.sourceData().size()).isEqualTo(4); + assertThat(sourceData.sourceData().size()).isEqualTo(2); assertThat(sourceData.sourceData().get("color-configmap.one")).isEqualTo("1"); assertThat(sourceData.sourceData().get("shape-configmap.two")).isEqualTo("2"); - assertThat(sourceData.sourceData().get("color-configmap-k8s.four")).isEqualTo("4"); - assertThat(sourceData.sourceData().get("shape-configmap-k8s.five")).isEqualTo("5"); - assertThat(sourceData.sourceName()) - .isEqualTo("configmap.color-configmap.color-configmap-k8s.shape-configmap.shape-configmap-k8s.default"); + assertThat(sourceData.sourceName()).isEqualTo("configmap.color-configmap.shape-configmap.default"); } @@ -525,7 +523,7 @@ void cache(CapturedOutput output) { NormalizedSource redNormalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, Collections.singletonMap("color", "red"), true, ConfigUtils.Prefix.DELAYED, true); Fabric8ConfigContext redContext = new Fabric8ConfigContext(mockClient, redNormalizedSource, NAMESPACE, - environment); + environment, NAMESPACED_BATCH_READ); Fabric8ContextToSourceData redData = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData redSourceData = redData.apply(redContext); @@ -536,7 +534,7 @@ void cache(CapturedOutput output) { NormalizedSource greenNormalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, Collections.singletonMap("color", "green"), true, ConfigUtils.Prefix.DELAYED, true); Fabric8ConfigContext greenContext = new Fabric8ConfigContext(mockClient, greenNormalizedSource, NAMESPACE, - environment); + environment, NAMESPACED_BATCH_READ); Fabric8ContextToSourceData greenData = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData greenSourceData = greenData.apply(greenContext); diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledConfigMapContextToSourceDataProviderNonNamespacedBatchReadTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledConfigMapContextToSourceDataProviderNonNamespacedBatchReadTests.java new file mode 100644 index 0000000000..7aa9e038c0 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledConfigMapContextToSourceDataProviderNonNamespacedBatchReadTests.java @@ -0,0 +1,513 @@ +/* + * Copyright 2012-2024 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.Iterator; +import java.util.LinkedHashMap; +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.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.kubernetes.commons.config.ConfigUtils; +import org.springframework.cloud.kubernetes.commons.config.LabeledConfigMapNormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.SourceData; +import org.springframework.mock.env.MockEnvironment; + +/** + * @author wind57 + */ +@EnableKubernetesMockClient(crud = true, https = false) +@ExtendWith(OutputCaptureExtension.class) +class LabeledConfigMapContextToSourceDataProviderNonNamespacedBatchReadTests { + + private static final boolean NAMESPACED_BATCH_READ = false; + + 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.configMaps().inNamespace(NAMESPACE).delete(); + new Fabric8SourcesNamespaceBatched().discardConfigMaps(); + } + + /** + * we have a single config map deployed. it has two labels and these match against our + * queries. + */ + @Test + void singleConfigMapMatchAgainstLabels() { + + ConfigMap configMap = new ConfigMapBuilder().withNewMetadata() + .withName("test-configmap") + .withLabels(LABELS) + .endMetadata() + .addToData("name", "value") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(configMap).create(); + + NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, LABELS, true, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.test-configmap.default"); + Assertions.assertThat(sourceData.sourceData()).containsExactlyInAnyOrderEntriesOf(Map.of("name", "value")); + + } + + /** + * we have three configmaps deployed. two of them have labels that match (color=red), + * one does not (color=blue). + */ + @Test + void twoConfigMapsMatchAgainstLabels() { + + ConfigMap redOne = new ConfigMapBuilder().withNewMetadata() + .withName("red-configmap") + .withLabels(RED_LABEL) + .endMetadata() + .addToData("colorOne", "really-red") + .build(); + + ConfigMap redTwo = new ConfigMapBuilder().withNewMetadata() + .withName("red-configmap-again") + .withLabels(RED_LABEL) + .endMetadata() + .addToData("colorTwo", "really-red-again") + .build(); + + ConfigMap blue = new ConfigMapBuilder().withNewMetadata() + .withName("blue-configmap") + .withLabels(BLUE_LABEL) + .endMetadata() + .addToData("color", "blue") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(redOne).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(redTwo).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(blue).create(); + + NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, RED_LABEL, true, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red-configmap.red-configmap-again.default"); + Assertions.assertThat(sourceData.sourceData()).hasSize(2); + Assertions.assertThat(sourceData.sourceData().get("colorOne")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData().get("colorTwo")).isEqualTo("really-red-again"); + + } + + /** + * one configmap deployed (pink), does not match our query (blue). + */ + @Test + void configMapNoMatch() { + + ConfigMap pink = new ConfigMapBuilder().withNewMetadata() + .withName("pink-configmap") + .withLabels(PINK_LABEL) + .endMetadata() + .addToData("color", "pink") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(pink).create(); + + NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, BLUE_LABEL, true, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.color.default"); + Assertions.assertThat(sourceData.sourceData()).isEmpty(); + } + + /** + * LabeledConfigMapContextToSourceDataProvider 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("test-configmap") + .withLabels(LABELS) + .endMetadata() + .addToData("name", "value") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(configMap).create(); + + // different namespace + NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE + "nope", LABELS, true, + false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.test-configmap.default"); + Assertions.assertThat(sourceData.sourceData()).containsExactlyInAnyOrderEntriesOf(Map.of("name", "value")); + } + + /** + * one configmap with name : "blue-configmap" and labels "color=blue" is deployed. we + * search it with the same labels, find it, and assert that name of the SourceData (it + * must use its name, not its labels) and values in the SourceData must be prefixed + * (since we have provided an explicit prefix). + */ + @Test + void testWithPrefix() { + ConfigMap configMap = new ConfigMapBuilder().withNewMetadata() + .withName("blue-configmap") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("what-color", "blue-color") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(configMap).create(); + + ConfigUtils.Prefix mePrefix = ConfigUtils.findPrefix("me", false, false, "irrelevant"); + NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "blue"), true, mePrefix, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.blue-configmap.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("me.what-color", "blue-color")); + } + + /** + * two configmaps are deployed (name:blue-configmap, name:another-blue-configmap) and + * labels "color=blue" (on both). we search with the same labels, find them, and + * assert that name of the SourceData (it must use its name, not its labels) and + * values in the SourceData must be prefixed (since we have provided a delayed + * prefix). + * + * Also notice that the prefix is made up from both configmap names. + * + */ + @Test + void testTwoConfigmapsWithPrefix() { + ConfigMap blueConfigMap = new ConfigMapBuilder().withNewMetadata() + .withName("blue-configmap") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("first", "blue") + .build(); + + ConfigMap anotherBlue = new ConfigMapBuilder().withNewMetadata() + .withName("another-blue-configmap") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("second", "blue") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(blueConfigMap).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(anotherBlue).create(); + + NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DELAYED, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()) + .isEqualTo("configmap.another-blue-configmap.blue-configmap.default"); + + Map properties = sourceData.sourceData(); + Assertions.assertThat(properties).hasSize(2); + Iterator keys = properties.keySet().iterator(); + String firstKey = keys.next(); + String secondKey = keys.next(); + + if (firstKey.contains("first")) { + Assertions.assertThat(firstKey).isEqualTo("blue-configmap.first"); + } + + Assertions.assertThat(secondKey).isEqualTo("another-blue-configmap.second"); + Assertions.assertThat(properties.get(firstKey)).isEqualTo("blue"); + Assertions.assertThat(properties.get(secondKey)).isEqualTo("blue"); + } + + /** + * two configmaps are deployed: "color-configmap" with label: "{color:blue}" and + * "color-configmap-k8s" with no labels. We search by "{color:red}", do not find + * anything and thus have an empty SourceData. + */ + @Test + void searchWithLabelsNoConfigmapsFound() { + ConfigMap colorConfigmap = new ConfigMapBuilder().withNewMetadata() + .withName("color-configmap") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("one", "1") + .build(); + + ConfigMap colorConfigmapK8s = new ConfigMapBuilder().withNewMetadata() + .withName("color-configmap-k8s") + .endMetadata() + .addToData("two", "2") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(colorConfigmap).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(colorConfigmapK8s).create(); + MockEnvironment environment = new MockEnvironment(); + + NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "red"), true, ConfigUtils.Prefix.DEFAULT, true); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).isEmpty(); + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.color.default"); + + } + + /** + * two configmaps are deployed: "color-configmap" with label: "{color:blue}" and + * "shape-configmap" with label: "{shape:round}". We search by "{color:blue}" and find + * one configmap. + */ + @Test + void searchWithLabelsOneConfigMapFound() { + ConfigMap colorConfigmap = new ConfigMapBuilder().withNewMetadata() + .withName("color-configmap") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("one", "1") + .build(); + + ConfigMap shapeConfigmap = new ConfigMapBuilder().withNewMetadata() + .withName("shape-configmap") + .endMetadata() + .addToData("two", "2") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(colorConfigmap).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(shapeConfigmap).create(); + MockEnvironment environment = new MockEnvironment(); + + NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DEFAULT, true); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).hasSize(1); + Assertions.assertThat(sourceData.sourceData().get("one")).isEqualTo("1"); + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.color-configmap.default"); + + } + + /** + *
+	 *     - configmap "color-configmap" with label "{color:blue}"
+	 *     - configmap "shape-configmap" with labels "{color:blue, shape:round}"
+	 *     - configmap "no-fit" with labels "{tag:no-fit}"
+	 *     - configmap "color-configmap-k8s" with label "{color:red}"
+	 *     - configmap "shape-configmap-k8s" with label "{shape:triangle}"
+	 * 
+ */ + @Test + void searchWithLabelsTwoConfigMapsFound() { + ConfigMap colorConfigMap = new ConfigMapBuilder().withNewMetadata() + .withName("color-configmap") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("one", "1") + .build(); + + ConfigMap shapeConfigmap = new ConfigMapBuilder().withNewMetadata() + .withName("shape-configmap") + .withLabels(Map.of("color", "blue", "shape", "round")) + .endMetadata() + .addToData("two", "2") + .build(); + + ConfigMap noFit = new ConfigMapBuilder().withNewMetadata() + .withName("no-fit") + .withLabels(Map.of("tag", "no-fit")) + .endMetadata() + .addToData("three", "3") + .build(); + + ConfigMap colorConfigmapK8s = new ConfigMapBuilder().withNewMetadata() + .withName("color-configmap-k8s") + .withLabels(Map.of("color", "red")) + .endMetadata() + .addToData("four", "4") + .build(); + + ConfigMap shapeConfigmapK8s = new ConfigMapBuilder().withNewMetadata() + .withName("shape-configmap-k8s") + .withLabels(Map.of("shape", "triangle")) + .endMetadata() + .addToData("five", "5") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(colorConfigMap).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(shapeConfigmap).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(noFit).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(colorConfigmapK8s).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(shapeConfigmapK8s).create(); + + MockEnvironment environment = new MockEnvironment(); + + NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DELAYED, true); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).hasSize(2); + Assertions.assertThat(sourceData.sourceData().get("color-configmap.one")).isEqualTo("1"); + Assertions.assertThat(sourceData.sourceData().get("shape-configmap.two")).isEqualTo("2"); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.color-configmap.shape-configmap.default"); + + } + + /** + *
+	 *     - configmap "red-configmap" with label "{color:red}"
+	 *     - configmap "green-configmap" with labels "{color:green}"
+	 *     - we first search for "red" and find it, and it is retrieved from the cluster via the client.
+	 * 	   - we then search for the "green" one, and it is not retrieved from the cache.
+	 * 
+ */ + @Test + void nonCache(CapturedOutput output) { + ConfigMap redConfigMap = new ConfigMapBuilder().withNewMetadata() + .withName("red-configmap") + .withLabels(Collections.singletonMap("color", "red")) + .endMetadata() + .addToData("one", "1") + .build(); + + ConfigMap greenConfigmap = new ConfigMapBuilder().withNewMetadata() + .withName("green-configmap") + .withLabels(Map.of("color", "green")) + .endMetadata() + .addToData("two", "2") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(redConfigMap).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(greenConfigmap).create(); + + MockEnvironment environment = new MockEnvironment(); + + NormalizedSource redNormalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "red"), true, ConfigUtils.Prefix.DELAYED, true); + Fabric8ConfigContext redContext = new Fabric8ConfigContext(mockClient, redNormalizedSource, NAMESPACE, + environment, NAMESPACED_BATCH_READ); + Fabric8ContextToSourceData redData = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData redSourceData = redData.apply(redContext); + + Assertions.assertThat(redSourceData.sourceData()).hasSize(1); + Assertions.assertThat(redSourceData.sourceData().get("red-configmap.one")).isEqualTo("1"); + + Assertions.assertThat(output.getAll()) + .doesNotContain("Loaded all config maps in namespace '" + NAMESPACE + "'"); + Assertions.assertThat(output.getOut()).contains("Will read individual configmaps in namespace"); + + NormalizedSource greenNormalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "green"), true, ConfigUtils.Prefix.DELAYED, true); + Fabric8ConfigContext greenContext = new Fabric8ConfigContext(mockClient, greenNormalizedSource, NAMESPACE, + environment, NAMESPACED_BATCH_READ); + Fabric8ContextToSourceData greenData = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData greenSourceData = greenData.apply(greenContext); + + Assertions.assertThat(greenSourceData.sourceData()).hasSize(1); + Assertions.assertThat(greenSourceData.sourceData().get("green-configmap.two")).isEqualTo("2"); + + // meaning there is a single entry with such a log statement + String[] out = output.getAll().split("Loaded all config maps in namespace"); + Assertions.assertThat(out.length).isEqualTo(1); + + // meaning that both reads were non cached + out = output.getAll().split("Will read individual configmaps in namespace"); + Assertions.assertThat(out.length).isEqualTo(3); + + } + +} 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/LabeledSecretContextToSourceDataProviderNamespacedBatchReadTests.java similarity index 92% rename from spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledSecretContextToSourceDataProviderTests.java rename to spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledSecretContextToSourceDataProviderNamespacedBatchReadTests.java index 8b394fad9f..2c8e8c4692 100644 --- 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/LabeledSecretContextToSourceDataProviderNamespacedBatchReadTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2024 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. @@ -50,7 +50,9 @@ */ @EnableKubernetesMockClient(crud = true, https = false) @ExtendWith(OutputCaptureExtension.class) -class LabeledSecretContextToSourceDataProviderTests { +class LabeledSecretContextToSourceDataProviderNamespacedBatchReadTests { + + private static final boolean NAMESPACED_BATCH_READ = true; private static final String NAMESPACE = "default"; @@ -64,14 +66,12 @@ class LabeledSecretContextToSourceDataProviderTests { private static KubernetesClient mockClient; - static { - LABELS.put("label2", "value2"); - LABELS.put("label1", "value1"); - } - @BeforeAll static void beforeAll() { + LABELS.put("label2", "value2"); + LABELS.put("label1", "value1"); + // 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"); @@ -85,7 +85,7 @@ static void beforeAll() { @AfterEach void afterEach() { mockClient.secrets().inNamespace(NAMESPACE).delete(); - new Fabric8SecretsCache().discardAll(); + new Fabric8SourcesNamespaceBatched().discardSecrets(); } /** @@ -104,9 +104,9 @@ void singleSecretMatchAgainstLabels() { mockClient.secrets().inNamespace(NAMESPACE).resource(secret).create(); - NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, LABELS, true, false); + NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, LABELS, true); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -149,9 +149,9 @@ void twoSecretsMatchAgainstLabels() { mockClient.secrets().inNamespace(NAMESPACE).resource(redTwo).create(); mockClient.secrets().inNamespace(NAMESPACE).resource(blue).create(); - NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, RED_LABEL, true, false); + NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, RED_LABEL, true); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -178,9 +178,9 @@ void secretNoMatch() { mockClient.secrets().inNamespace(NAMESPACE).resource(pink).create(); - NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, BLUE_LABEL, true, false); + NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, BLUE_LABEL, true); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -208,9 +208,9 @@ void namespaceMatch() { mockClient.secrets().inNamespace(NAMESPACE).resource(secret).create(); // different namespace - NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE + "nope", LABELS, true, false); + NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE + "nope", LABELS, true); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -239,9 +239,9 @@ void testWithPrefix() { ConfigUtils.Prefix mePrefix = ConfigUtils.findPrefix("me", false, false, "irrelevant"); NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, - Collections.singletonMap("color", "blue"), true, mePrefix, false); + Collections.singletonMap("color", "blue"), true, mePrefix); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -280,9 +280,9 @@ void testTwoSecretsWithPrefix() { mockClient.secrets().inNamespace(NAMESPACE).resource(anotherBlue).create(); NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, - Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DELAYED, false); + Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DELAYED); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -307,8 +307,7 @@ void testTwoSecretsWithPrefix() { /** * two secrets are deployed: secret "color-secret" with label: "{color:blue}" and * "color-secret-k8s" with no labels. We search by "{color:red}", do not find anything - * and thus have an empty SourceData. profile based sources are enabled, but it has no - * effect. + * and thus have an empty SourceData. */ @Test void searchWithLabelsNoSecretFound() { @@ -328,11 +327,11 @@ void searchWithLabelsNoSecretFound() { mockClient.secrets().inNamespace(NAMESPACE).resource(colorSecret).create(); mockClient.secrets().inNamespace(NAMESPACE).resource(colorSecretK8s).create(); MockEnvironment environment = new MockEnvironment(); - environment.setActiveProfiles("k8s"); NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, - Collections.singletonMap("color", "red"), true, ConfigUtils.Prefix.DEFAULT, true); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment); + Collections.singletonMap("color", "red"), true, ConfigUtils.Prefix.DEFAULT); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -345,7 +344,7 @@ void searchWithLabelsNoSecretFound() { /** * two secrets are deployed: secret "color-secret" with label: "{color:blue}" and * "shape-secret" with label: "{shape:round}". We search by "{color:blue}" and find - * one secret. profile based sources are enabled, but it has no effect. + * one secret. */ @Test void searchWithLabelsOneSecretFound() { @@ -365,11 +364,11 @@ void searchWithLabelsOneSecretFound() { mockClient.secrets().inNamespace(NAMESPACE).resource(colorSecret).create(); mockClient.secrets().inNamespace(NAMESPACE).resource(shapeSecret).create(); MockEnvironment environment = new MockEnvironment(); - environment.setActiveProfiles("k8s"); NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, - Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DEFAULT, true); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment); + Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DEFAULT); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -408,16 +407,16 @@ void searchWithLabelsOneSecretFoundAndOneFromProfileFound() { environment.setActiveProfiles("k8s"); NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, - Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DELAYED, true); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment); + Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DELAYED); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + true); Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); - Assertions.assertThat(sourceData.sourceData().size()).isEqualTo(2); + Assertions.assertThat(sourceData.sourceData().size()).isEqualTo(1); Assertions.assertThat(sourceData.sourceData().get("color-secret.one")).isEqualTo("1"); - Assertions.assertThat(sourceData.sourceData().get("color-secret-k8s.two")).isEqualTo("2"); - Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.color-secret.color-secret-k8s.default"); + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.color-secret.default"); } @@ -431,7 +430,7 @@ void searchWithLabelsOneSecretFoundAndOneFromProfileFound() { * */ @Test - void searchWithLabelsTwoSecretsFoundAndOneFromProfileFound() { + void searchWithLabelsTwoSecretsFound() { Secret colorSecret = new SecretBuilder().withNewMetadata() .withName("color-secret") .withLabels(Collections.singletonMap("color", "blue")) @@ -474,23 +473,20 @@ void searchWithLabelsTwoSecretsFoundAndOneFromProfileFound() { mockClient.secrets().inNamespace(NAMESPACE).resource(shapeSecretK8s).create(); MockEnvironment environment = new MockEnvironment(); - environment.setActiveProfiles("k8s"); NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, - Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DELAYED, true); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment); + Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DELAYED); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); - Assertions.assertThat(sourceData.sourceData().size()).isEqualTo(4); + Assertions.assertThat(sourceData.sourceData().size()).isEqualTo(2); assertThat(sourceData.sourceData().get("color-secret.one")).isEqualTo("1"); assertThat(sourceData.sourceData().get("shape-secret.two")).isEqualTo("2"); - assertThat(sourceData.sourceData().get("color-secret-k8s.four")).isEqualTo("4"); - assertThat(sourceData.sourceData().get("shape-secret-k8s.five")).isEqualTo("5"); - assertThat(sourceData.sourceName()) - .isEqualTo("secret.color-secret.color-secret-k8s.shape-secret.shape-secret-k8s.default"); + assertThat(sourceData.sourceName()).isEqualTo("secret.color-secret.shape-secret.default"); } @@ -509,9 +505,9 @@ void testYaml() { mockClient.secrets().inNamespace(NAMESPACE).resource(colorSecret).create(); NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, - Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DEFAULT, true); + Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DEFAULT); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -551,9 +547,9 @@ void cache(CapturedOutput output) { MockEnvironment environment = new MockEnvironment(); NormalizedSource redNormalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, - Collections.singletonMap("color", "red"), true, ConfigUtils.Prefix.DELAYED, true); + Collections.singletonMap("color", "red"), true, ConfigUtils.Prefix.DELAYED); Fabric8ConfigContext redContext = new Fabric8ConfigContext(mockClient, redNormalizedSource, NAMESPACE, - environment); + environment, NAMESPACED_BATCH_READ); Fabric8ContextToSourceData redData = new LabeledSecretContextToSourceDataProvider().get(); SourceData redSourceData = redData.apply(redContext); @@ -562,9 +558,9 @@ void cache(CapturedOutput output) { Assertions.assertThat(output.getAll()).contains("Loaded all secrets in namespace '" + NAMESPACE + "'"); NormalizedSource greenNormalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, - Collections.singletonMap("color", "green"), true, ConfigUtils.Prefix.DELAYED, true); + Collections.singletonMap("color", "green"), true, ConfigUtils.Prefix.DELAYED); Fabric8ConfigContext greenContext = new Fabric8ConfigContext(mockClient, greenNormalizedSource, NAMESPACE, - environment); + environment, NAMESPACED_BATCH_READ); Fabric8ContextToSourceData greenData = new LabeledSecretContextToSourceDataProvider().get(); SourceData greenSourceData = greenData.apply(greenContext); diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledSecretContextToSourceDataProviderNonNamespacedBatchReadTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledSecretContextToSourceDataProviderNonNamespacedBatchReadTests.java new file mode 100644 index 0000000000..b35a29d250 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledSecretContextToSourceDataProviderNonNamespacedBatchReadTests.java @@ -0,0 +1,537 @@ +/* + * Copyright 2012-2024 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.Iterator; +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.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.kubernetes.commons.config.ConfigUtils; +import org.springframework.cloud.kubernetes.commons.config.LabeledSecretNormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.SourceData; +import org.springframework.mock.env.MockEnvironment; + +/** + * @author wind57 + */ +@EnableKubernetesMockClient(crud = true, https = false) +@ExtendWith(OutputCaptureExtension.class) +class LabeledSecretContextToSourceDataProviderNonNamespacedBatchReadTests { + + private static final boolean NAMESPACED_BATCH_READ = false; + + 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; + + @BeforeAll + static void beforeAll() { + + LABELS.put("label2", "value2"); + LABELS.put("label1", "value1"); + + // 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(); + new Fabric8SourcesNamespaceBatched().discardSecrets(); + } + + /** + * 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).resource(secret).create(); + + NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, LABELS, true); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.test-secret.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("secretName", "secretValue")); + + } + + /** + * we have three secrets 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).resource(redOne).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(redTwo).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(blue).create(); + + NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, RED_LABEL, true); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red-secret.red-secret-again.default"); + Assertions.assertThat(sourceData.sourceData()).hasSize(2); + Assertions.assertThat(sourceData.sourceData().get("colorOne")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData().get("colorTwo")).isEqualTo("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).resource(pink).create(); + + NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, BLUE_LABEL, true); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.color.default"); + Assertions.assertThat(sourceData.sourceData()).isEmpty(); + } + + /** + * 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).resource(secret).create(); + + // different namespace + NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE + "nope", LABELS, true); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.test-secret.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("secretName", "secretValue")); + } + + /** + * one secret with name : "blue-secret" and labels "color=blue" is deployed. we search + * it with the same labels, find it, and assert that name of the SourceData (it must + * use its name, not its labels) and values in the SourceData must be prefixed (since + * we have provided an explicit prefix). + */ + @Test + void testWithPrefix() { + Secret secret = new SecretBuilder().withNewMetadata() + .withName("blue-secret") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("what-color", Base64.getEncoder().encodeToString("blue-color".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(secret).create(); + + ConfigUtils.Prefix mePrefix = ConfigUtils.findPrefix("me", false, false, "irrelevant"); + NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "blue"), true, mePrefix); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.blue-secret.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("me.what-color", "blue-color")); + } + + /** + * two secrets are deployed (name:blue-secret, name:another-blue-secret) and labels + * "color=blue" (on both). we search with the same labels, find them, and assert that + * name of the SourceData (it must use its name, not its labels) and values in the + * SourceData must be prefixed (since we have provided a delayed prefix). + * + * Also notice that the prefix is made up from both secret names. + * + */ + @Test + void testTwoSecretsWithPrefix() { + Secret blueSecret = new SecretBuilder().withNewMetadata() + .withName("blue-secret") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("first", Base64.getEncoder().encodeToString("blue".getBytes())) + .build(); + + Secret anotherBlue = new SecretBuilder().withNewMetadata() + .withName("another-blue-secret") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("second", Base64.getEncoder().encodeToString("blue".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(blueSecret).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(anotherBlue).create(); + + NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DELAYED); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.another-blue-secret.blue-secret.default"); + + Map properties = sourceData.sourceData(); + Assertions.assertThat(properties).hasSize(2); + Iterator keys = properties.keySet().iterator(); + String firstKey = keys.next(); + String secondKey = keys.next(); + + if (firstKey.contains("first")) { + Assertions.assertThat(firstKey).isEqualTo("blue-secret.first"); + } + + Assertions.assertThat(secondKey).isEqualTo("another-blue-secret.second"); + Assertions.assertThat(properties.get(firstKey)).isEqualTo("blue"); + Assertions.assertThat(properties.get(secondKey)).isEqualTo("blue"); + } + + /** + * two secrets are deployed: secret "color-secret" with label: "{color:blue}" and + * "color-secret-k8s" with no labels. We search by "{color:red}", do not find anything + * and thus have an empty SourceData. + */ + @Test + void searchWithLabelsNoSecretFound() { + Secret colorSecret = new SecretBuilder().withNewMetadata() + .withName("color-secret") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("one", Base64.getEncoder().encodeToString("1".getBytes())) + .build(); + + Secret colorSecretK8s = new SecretBuilder().withNewMetadata() + .withName("color-secret-k8s") + .endMetadata() + .addToData("two", Base64.getEncoder().encodeToString("2".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(colorSecret).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(colorSecretK8s).create(); + MockEnvironment environment = new MockEnvironment(); + + NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "red"), true, ConfigUtils.Prefix.DEFAULT); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).isEmpty(); + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.color.default"); + + } + + /** + * two secrets are deployed: secret "color-secret" with label: "{color:blue}" and + * "shape-secret" with label: "{shape:round}". We search by "{color:blue}" and find + * one secret. + */ + @Test + void searchWithLabelsOneSecretFound() { + Secret colorSecret = new SecretBuilder().withNewMetadata() + .withName("color-secret") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("one", Base64.getEncoder().encodeToString("1".getBytes())) + .build(); + + Secret shapeSecret = new SecretBuilder().withNewMetadata() + .withName("shape-secret") + .endMetadata() + .addToData("two", Base64.getEncoder().encodeToString("2".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(colorSecret).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(shapeSecret).create(); + MockEnvironment environment = new MockEnvironment(); + + NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DEFAULT); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).hasSize(1); + Assertions.assertThat(sourceData.sourceData().get("one")).isEqualTo("1"); + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.color-secret.default"); + + } + + /** + *
+	 *     - secret "color-secret" with label "{color:blue}"
+	 *     - secret "shape-secret" with labels "{color:blue, shape:round}"
+	 *     - secret "no-fit" with labels "{tag:no-fit}"
+	 *     - secret "color-secret-k8s" with label "{color:red}"
+	 *     - secret "shape-secret-k8s" with label "{shape:triangle}"
+	 * 
+ */ + @Test + void searchWithLabelsTwoSecretsFound() { + Secret colorSecret = new SecretBuilder().withNewMetadata() + .withName("color-secret") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("one", Base64.getEncoder().encodeToString("1".getBytes())) + .build(); + + Secret shapeSecret = new SecretBuilder().withNewMetadata() + .withName("shape-secret") + .withLabels(Map.of("color", "blue", "shape", "round")) + .endMetadata() + .addToData("two", Base64.getEncoder().encodeToString("2".getBytes())) + .build(); + + Secret noFit = new SecretBuilder().withNewMetadata() + .withName("no-fit") + .withLabels(Map.of("tag", "no-fit")) + .endMetadata() + .addToData("three", Base64.getEncoder().encodeToString("3".getBytes())) + .build(); + + Secret colorSecretK8s = new SecretBuilder().withNewMetadata() + .withName("color-secret-k8s") + .withLabels(Map.of("color", "red")) + .endMetadata() + .addToData("four", Base64.getEncoder().encodeToString("4".getBytes())) + .build(); + + Secret shapeSecretK8s = new SecretBuilder().withNewMetadata() + .withName("shape-secret-k8s") + .withLabels(Map.of("shape", "triangle")) + .endMetadata() + .addToData("five", Base64.getEncoder().encodeToString("5".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(colorSecret).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(shapeSecret).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(noFit).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(colorSecretK8s).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(shapeSecretK8s).create(); + + MockEnvironment environment = new MockEnvironment(); + + NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DELAYED); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).hasSize(2); + Assertions.assertThat(sourceData.sourceData().get("color-secret.one")).isEqualTo("1"); + Assertions.assertThat(sourceData.sourceData().get("shape-secret.two")).isEqualTo("2"); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.color-secret.shape-secret.default"); + + } + + /** + * yaml/properties gets special treatment + */ + @Test + void testYaml() { + Secret colorSecret = new SecretBuilder().withNewMetadata() + .withName("color-secret") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("test.yaml", Base64.getEncoder().encodeToString("color: blue".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(colorSecret).create(); + + NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DEFAULT); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).hasSize(1); + Assertions.assertThat(sourceData.sourceData().get("color")).isEqualTo("blue"); + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.color-secret.default"); + } + + /** + *
+	 *     - secret "red" with label "{color:red}"
+	 *     - secret "green" with labels "{color:green}"
+	 *     - we first search for "red" and find it, and it is retrieved from the cluster via the client.
+	 * 	   - we then search for the "green" one, and it is retrieved from the cache this time.
+	 * 
+ */ + @Test + void cache(CapturedOutput output) { + Secret red = new SecretBuilder().withNewMetadata() + .withName("red") + .withLabels(Collections.singletonMap("color", "red")) + .endMetadata() + .addToData("one", Base64.getEncoder().encodeToString("1".getBytes())) + .build(); + + Secret green = new SecretBuilder().withNewMetadata() + .withName("green") + .withLabels(Map.of("color", "green")) + .endMetadata() + .addToData("two", Base64.getEncoder().encodeToString("2".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(red).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(green).create(); + + MockEnvironment environment = new MockEnvironment(); + + NormalizedSource redNormalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "red"), true, ConfigUtils.Prefix.DELAYED); + Fabric8ConfigContext redContext = new Fabric8ConfigContext(mockClient, redNormalizedSource, NAMESPACE, + environment, NAMESPACED_BATCH_READ); + Fabric8ContextToSourceData redData = new LabeledSecretContextToSourceDataProvider().get(); + SourceData redSourceData = redData.apply(redContext); + + Assertions.assertThat(redSourceData.sourceData()).hasSize(1); + Assertions.assertThat(redSourceData.sourceData().get("red.one")).isEqualTo("1"); + + Assertions.assertThat(output.getAll()).doesNotContain("Loaded all secrets in namespace '" + NAMESPACE + "'"); + Assertions.assertThat(output.getAll()).contains("Will read individual secrets in namespace"); + + NormalizedSource greenNormalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "green"), true, ConfigUtils.Prefix.DELAYED); + Fabric8ConfigContext greenContext = new Fabric8ConfigContext(mockClient, greenNormalizedSource, NAMESPACE, + environment, NAMESPACED_BATCH_READ); + Fabric8ContextToSourceData greenData = new LabeledSecretContextToSourceDataProvider().get(); + SourceData greenSourceData = greenData.apply(greenContext); + + Assertions.assertThat(greenSourceData.sourceData()).hasSize(1); + Assertions.assertThat(greenSourceData.sourceData().get("green.two")).isEqualTo("2"); + + // meaning there is a single entry with such a log statement + String[] out = output.getAll().split("Loaded all secrets in namespace"); + Assertions.assertThat(out.length).isEqualTo(1); + + // meaning that the second read was done from the cache + out = output.getAll().split("Will read individual secrets in namespace"); + Assertions.assertThat(out.length).isEqualTo(3); + + } + +} 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/NamedConfigMapContextToSourceDataProviderNamespacedBatchReadTests.java similarity index 94% rename from spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/NamedConfigMapContextToSourceDataProviderTests.java rename to spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/NamedConfigMapContextToSourceDataProviderNamespacedBatchReadTests.java index 1d48d22354..2da854bd08 100644 --- 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/NamedConfigMapContextToSourceDataProviderNamespacedBatchReadTests.java @@ -44,7 +44,9 @@ */ @EnableKubernetesMockClient(crud = true, https = false) @ExtendWith(OutputCaptureExtension.class) -class NamedConfigMapContextToSourceDataProviderTests { +class NamedConfigMapContextToSourceDataProviderNamespacedBatchReadTests { + + private static final boolean NAMESPACED_BATCH_READ = true; private static final String NAMESPACE = "default"; @@ -70,7 +72,7 @@ static void beforeAll() { @AfterEach void afterEach() { mockClient.configMaps().inNamespace(NAMESPACE).delete(); - new Fabric8ConfigMapsCache().discardAll(); + new Fabric8SourcesNamespaceBatched().discardConfigMaps(); } /** @@ -92,7 +94,7 @@ void noMatch() { NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("blue", NAMESPACE, true, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -121,7 +123,7 @@ void match() { NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("red", NAMESPACE, true, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -162,7 +164,8 @@ void matchIncludeSingleProfile() { NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("red", NAMESPACE, true, ConfigUtils.Prefix.DEFAULT, true, true); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env, + NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -206,7 +209,8 @@ void matchIncludeSingleProfileWithPrefix() { env.setActiveProfiles("with-profile"); NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("red", NAMESPACE, true, PREFIX, true); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env, + NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -255,7 +259,8 @@ void matchIncludeTwoProfilesWithPrefix() { env.setActiveProfiles("with-taste", "with-shape"); NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("red", NAMESPACE, true, PREFIX, true); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env, + NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -285,7 +290,7 @@ void matchWithName() { NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("application", NAMESPACE, true, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -315,7 +320,7 @@ void namespaceMatch() { String wrongNamespace = NAMESPACE + "nope"; NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("red", wrongNamespace, true, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -342,7 +347,7 @@ void testSingleYaml() { NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("red", NAMESPACE, true, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -372,7 +377,8 @@ void testCorrectNameWithProfile() { environment.setActiveProfiles("k8s"); NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("one", NAMESPACE, true, true); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -409,7 +415,8 @@ void cache(CapturedOutput output) { MockEnvironment env = new MockEnvironment(); NormalizedSource redNormalizedSource = new NamedConfigMapNormalizedSource("red", NAMESPACE, true, PREFIX, false); - Fabric8ConfigContext redContext = new Fabric8ConfigContext(mockClient, redNormalizedSource, NAMESPACE, env); + Fabric8ConfigContext redContext = new Fabric8ConfigContext(mockClient, redNormalizedSource, NAMESPACE, env, + NAMESPACED_BATCH_READ); Fabric8ContextToSourceData redData = new NamedConfigMapContextToSourceDataProvider().get(); SourceData redSourceData = redData.apply(redContext); @@ -417,10 +424,12 @@ void cache(CapturedOutput output) { Assertions.assertThat(redSourceData.sourceData().size()).isEqualTo(1); Assertions.assertThat(redSourceData.sourceData().get("some.color")).isEqualTo("really-red"); Assertions.assertThat(output.getAll()).contains("Loaded all config maps in namespace '" + NAMESPACE + "'"); + Assertions.assertThat(output.getAll()).doesNotContain("Will read individual configmaps in namespace"); NormalizedSource greenNormalizedSource = new NamedConfigMapNormalizedSource("green", NAMESPACE, true, PREFIX, false); - Fabric8ConfigContext greenContext = new Fabric8ConfigContext(mockClient, greenNormalizedSource, NAMESPACE, env); + Fabric8ConfigContext greenContext = new Fabric8ConfigContext(mockClient, greenNormalizedSource, NAMESPACE, env, + NAMESPACED_BATCH_READ); Fabric8ContextToSourceData greenData = new NamedConfigMapContextToSourceDataProvider().get(); SourceData greenSourceData = greenData.apply(greenContext); diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/NamedConfigMapContextToSourceDataProviderNonNamespacedBatchReadTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/NamedConfigMapContextToSourceDataProviderNonNamespacedBatchReadTests.java new file mode 100644 index 0000000000..4eadc5798b --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/NamedConfigMapContextToSourceDataProviderNonNamespacedBatchReadTests.java @@ -0,0 +1,459 @@ +/* + * Copyright 2012-2024 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.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.kubernetes.commons.config.ConfigUtils; +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.mock.env.MockEnvironment; + +/** + * @author wind57 + */ +@EnableKubernetesMockClient(crud = true, https = false) +@ExtendWith(OutputCaptureExtension.class) +class NamedConfigMapContextToSourceDataProviderNonNamespacedBatchReadTests { + + private static final boolean NAMESPACED_BATCH_READ = false; + + private static final String NAMESPACE = "default"; + + private static KubernetesClient mockClient; + + private static final ConfigUtils.Prefix PREFIX = ConfigUtils.findPrefix("some", false, false, "irrelevant"); + + private static final Map COLOR_REALLY_RED = Map.of("color", "really-red"); + + @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(); + new Fabric8SourcesNamespaceBatched().discardConfigMaps(); + } + + /** + *
+	 *     one configmap deployed with name "red"
+	 *     we search by name, but for the "blue" one, as such not find it
+	 * 
+ */ + @Test + void noMatch() { + + ConfigMap configMap = new ConfigMapBuilder().withNewMetadata() + .withName("red") + .endMetadata() + .addToData(COLOR_REALLY_RED) + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(configMap).create(); + + NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("blue", NAMESPACE, true, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.blue.default"); + Assertions.assertThat(sourceData.sourceData()).isEmpty(); + + } + + /** + *
+	 *     one configmap deployed with name "red"
+	 *     we search by name, for the "red" one, as such we find it
+	 * 
+ */ + @Test + void match() { + + ConfigMap configMap = new ConfigMapBuilder().withNewMetadata() + .withName("red") + .endMetadata() + .addToData(COLOR_REALLY_RED) + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(configMap).create(); + + NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("red", NAMESPACE, true, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red.default"); + Assertions.assertThat(sourceData.sourceData()).isEqualTo(COLOR_REALLY_RED); + + } + + /** + *
+	 *     - two configmaps deployed : "red" and "red-with-profile".
+	 *     - "red" is matched directly, "red-with-profile" is matched because we have an active profile
+	 *       "active-profile"
+	 * 
+ */ + @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).resource(red).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(redWithProfile).create(); + + // 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, + ConfigUtils.Prefix.DEFAULT, true, true); + + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env, + NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red.red-with-profile.default.with-profile"); + Assertions.assertThat(sourceData.sourceData()).hasSize(2); + Assertions.assertThat(sourceData.sourceData().get("color")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData().get("taste")).isEqualTo("mango"); + + } + + /** + *
+	 *     - two configmaps deployed : "red" and "red-with-profile".
+	 *     - "red" is matched directly, "red-with-profile" is matched because we have an active profile
+	 *       "active-profile"
+	 *     -  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).resource(red).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(redWithProfile).create(); + + // 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, PREFIX, true); + + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env, + NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red.red-with-profile.default"); + Assertions.assertThat(sourceData.sourceData()).hasSize(2); + Assertions.assertThat(sourceData.sourceData().get("some.color")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData().get("some.taste")).isEqualTo("mango"); + + } + + /** + *
+	 *     - three configmaps deployed : "red", "red-with-taste" and "red-with-shape"
+	 *     - "red" is matched directly, the other two are matched because of active profiles
+	 *     -  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).resource(red).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(redWithTaste).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(redWithShape).create(); + + // 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, PREFIX, true); + + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env, + NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red.red-with-shape.red-with-taste.default"); + Assertions.assertThat(sourceData.sourceData()).hasSize(3); + Assertions.assertThat(sourceData.sourceData().get("some.color")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData().get("some.taste")).isEqualTo("mango"); + Assertions.assertThat(sourceData.sourceData().get("some.shape")).isEqualTo("round"); + + } + + /** + *
+	 * 		proves that an implicit configmap is going to be generated and read, even if
+	 * 	    we did not provide one
+	 * 
+ */ + @Test + void matchWithName() { + ConfigMap configMap = new ConfigMapBuilder().withNewMetadata() + .withName("application") + .endMetadata() + .addToData("color", "red") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(configMap).create(); + + NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("application", NAMESPACE, true, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.application.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Collections.singletonMap("color", "red")); + } + + /** + *
+	 *     - NamedSecretContextToSourceDataProvider gets as input a KubernetesClientConfigContext
+	 *     - This context has a namespace as well as a NormalizedSource, that has a namespace too.
+	 *     - 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).resource(configMap).create(); + + String wrongNamespace = NAMESPACE + "nope"; + NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("red", wrongNamespace, true, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Collections.singletonMap("color", "really-red")); + } + + /** + *
+	 *     - proves that single yaml file gets special treatment
+	 * 
+ */ + @Test + void testSingleYaml() { + ConfigMap configMap = new ConfigMapBuilder().withNewMetadata() + .withName("red") + .endMetadata() + .addToData("single.yaml", "key: value") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(configMap).create(); + + NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("red", NAMESPACE, true, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Collections.singletonMap("key", "value")); + } + + /** + *
+	 *     - one configmap is deployed with name "one"
+	 *     - profile is enabled with name "k8s"
+	 *
+	 *     we assert that the name of the source is "one" and does not contain "one-dev"
+	 * 
+ */ + @Test + void testCorrectNameWithProfile() { + ConfigMap configMap = new ConfigMapBuilder().withNewMetadata() + .withName("one") + .endMetadata() + .addToData("key", "value") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(configMap).create(); + MockEnvironment environment = new MockEnvironment(); + environment.setActiveProfiles("k8s"); + + NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("one", NAMESPACE, true, true); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.one.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Collections.singletonMap("key", "value")); + } + + /** + *
+	 *     - two configmaps are deployed : "red", "green", in the same namespace.
+	 *     - we first search for "red" and find it, and it is retrieved from the cluster via the client.
+	 *     - we then search for the "green" one and it is not retrieved from the cache.
+	 * 
+ */ + @Test + void nonCache(CapturedOutput output) { + + ConfigMap red = new ConfigMapBuilder().withNewMetadata() + .withName("red") + .endMetadata() + .addToData(COLOR_REALLY_RED) + .build(); + + ConfigMap green = new ConfigMapBuilder().withNewMetadata() + .withName("green") + .endMetadata() + .addToData("taste", "mango") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(red).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(green).create(); + + MockEnvironment env = new MockEnvironment(); + NormalizedSource redNormalizedSource = new NamedConfigMapNormalizedSource("red", NAMESPACE, true, PREFIX, + false); + Fabric8ConfigContext redContext = new Fabric8ConfigContext(mockClient, redNormalizedSource, NAMESPACE, env, + NAMESPACED_BATCH_READ); + Fabric8ContextToSourceData redData = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData redSourceData = redData.apply(redContext); + + Assertions.assertThat(redSourceData.sourceName()).isEqualTo("configmap.red.default"); + Assertions.assertThat(redSourceData.sourceData()).hasSize(1); + Assertions.assertThat(redSourceData.sourceData().get("some.color")).isEqualTo("really-red"); + + Assertions.assertThat(output.getAll()) + .doesNotContain("Loaded all config maps in namespace '" + NAMESPACE + "'"); + Assertions.assertThat(output.getOut()) + .contains("Will read individual configmaps in namespace : default with names : [red]"); + + NormalizedSource greenNormalizedSource = new NamedConfigMapNormalizedSource("green", NAMESPACE, true, PREFIX, + false); + Fabric8ConfigContext greenContext = new Fabric8ConfigContext(mockClient, greenNormalizedSource, NAMESPACE, env, + NAMESPACED_BATCH_READ); + Fabric8ContextToSourceData greenData = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData greenSourceData = greenData.apply(greenContext); + + Assertions.assertThat(greenSourceData.sourceName()).isEqualTo("configmap.green.default"); + Assertions.assertThat(greenSourceData.sourceData()).hasSize(1); + Assertions.assertThat(greenSourceData.sourceData().get("some.taste")).isEqualTo("mango"); + + // meaning there is a no such entry with such a log statement + String[] out = output.getAll().split("Loaded all config maps in namespace"); + Assertions.assertThat(out.length).isEqualTo(1); + + // meaning that the second read was not from the cache + out = output.getAll().split("Will read individual configmaps in namespace : default"); + Assertions.assertThat(out.length).isEqualTo(3); + + } + +} 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/NamedSecretContextToSourceDataProviderNamespacedBatchReadTests.java similarity index 94% rename from spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/NamedSecretContextToSourceDataProviderTests.java rename to spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/NamedSecretContextToSourceDataProviderNamespacedBatchReadTests.java index d33dcb0975..62a8c8a02c 100644 --- 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/NamedSecretContextToSourceDataProviderNamespacedBatchReadTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2024 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. @@ -45,7 +45,9 @@ */ @EnableKubernetesMockClient(crud = true, https = false) @ExtendWith(OutputCaptureExtension.class) -class NamedSecretContextToSourceDataProviderTests { +class NamedSecretContextToSourceDataProviderNamespacedBatchReadTests { + + private static final boolean NAMESPACED_BATCH_READ = true; private static final String NAMESPACE = "default"; @@ -69,7 +71,7 @@ static void beforeAll() { @AfterEach void afterEach() { mockClient.secrets().inNamespace(NAMESPACE).delete(); - new Fabric8SecretsCache().discardAll(); + new Fabric8SourcesNamespaceBatched().discardSecrets(); } /** @@ -88,7 +90,7 @@ void singleSecretMatchAgainstLabels() { NormalizedSource normalizedSource = new NamedSecretNormalizedSource("red", NAMESPACE, true, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -130,7 +132,7 @@ void twoSecretMatchAgainstLabels() { NormalizedSource normalizedSource = new NamedSecretNormalizedSource("red", NAMESPACE, true, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -157,7 +159,7 @@ void testSecretNoMatch() { NormalizedSource normalizedSource = new NamedSecretNormalizedSource("blue", NAMESPACE, true, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -186,7 +188,7 @@ void namespaceMatch() { // different namespace NormalizedSource normalizedSource = new NamedSecretNormalizedSource("red", NAMESPACE + "nope", true, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -223,7 +225,8 @@ void matchIncludeSingleProfile() { env.setActiveProfiles("with-profile"); NormalizedSource normalizedSource = new NamedSecretNormalizedSource("red", NAMESPACE, true, ConfigUtils.Prefix.DEFAULT, true, true); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env, + NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -265,7 +268,8 @@ void matchIncludeSingleProfileWithPrefix() { env.setActiveProfiles("with-profile"); NormalizedSource normalizedSource = new NamedSecretNormalizedSource("red", NAMESPACE, true, PREFIX, true); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env, + NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -314,7 +318,8 @@ void matchIncludeTwoProfilesWithPrefix() { env.setActiveProfiles("with-taste", "with-shape"); NormalizedSource normalizedSource = new NamedSecretNormalizedSource("red", NAMESPACE, true, PREFIX, true); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env, + NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -346,7 +351,7 @@ void testSingleYaml() { // different namespace NormalizedSource normalizedSource = new NamedSecretNormalizedSource("single-yaml", NAMESPACE, true, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), NAMESPACED_BATCH_READ); Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -382,7 +387,8 @@ void cache(CapturedOutput output) { MockEnvironment env = new MockEnvironment(); NormalizedSource redNormalizedSource = new NamedSecretNormalizedSource("red", NAMESPACE, true, PREFIX, false); - Fabric8ConfigContext redContext = new Fabric8ConfigContext(mockClient, redNormalizedSource, NAMESPACE, env); + Fabric8ConfigContext redContext = new Fabric8ConfigContext(mockClient, redNormalizedSource, NAMESPACE, env, + NAMESPACED_BATCH_READ); Fabric8ContextToSourceData redData = new NamedSecretContextToSourceDataProvider().get(); SourceData redSourceData = redData.apply(redContext); @@ -390,10 +396,12 @@ void cache(CapturedOutput output) { Assertions.assertThat(redSourceData.sourceData().size()).isEqualTo(1); Assertions.assertThat(redSourceData.sourceData().get("some.color")).isEqualTo("red"); Assertions.assertThat(output.getAll()).contains("Loaded all secrets in namespace '" + NAMESPACE + "'"); + Assertions.assertThat(output.getAll()).doesNotContain("Will read individual secrets in namespace"); NormalizedSource greenNormalizedSource = new NamedSecretNormalizedSource("green", NAMESPACE, true, PREFIX, false); - Fabric8ConfigContext greenContext = new Fabric8ConfigContext(mockClient, greenNormalizedSource, NAMESPACE, env); + Fabric8ConfigContext greenContext = new Fabric8ConfigContext(mockClient, greenNormalizedSource, NAMESPACE, env, + NAMESPACED_BATCH_READ); Fabric8ContextToSourceData greenData = new NamedSecretContextToSourceDataProvider().get(); SourceData greenSourceData = greenData.apply(greenContext); diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/NamedSecretContextToSourceDataProviderNonNamespacedBatchReadTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/NamedSecretContextToSourceDataProviderNonNamespacedBatchReadTests.java new file mode 100644 index 0000000000..f1b7cd88d9 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/NamedSecretContextToSourceDataProviderNonNamespacedBatchReadTests.java @@ -0,0 +1,423 @@ +/* + * Copyright 2012-2024 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.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.kubernetes.commons.config.ConfigUtils; +import org.springframework.cloud.kubernetes.commons.config.NamedSecretNormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.SourceData; +import org.springframework.mock.env.MockEnvironment; + +/** + * @author wind57 + */ +@EnableKubernetesMockClient(crud = true, https = false) +@ExtendWith(OutputCaptureExtension.class) +class NamedSecretContextToSourceDataProviderNonNamespacedBatchReadTests { + + private static final boolean NAMESPACED_BATCH_READ = false; + + private static final String NAMESPACE = "default"; + + private static KubernetesClient mockClient; + + private static final ConfigUtils.Prefix PREFIX = ConfigUtils.findPrefix("some", false, false, "irrelevant"); + + @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(); + new Fabric8SourcesNamespaceBatched().discardSecrets(); + } + + /** + * 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).resource(secret).create(); + + NormalizedSource normalizedSource = new NamedSecretNormalizedSource("red", NAMESPACE, true, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(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-yellow".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(red).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(blue).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(yellow).create(); + + NormalizedSource normalizedSource = new NamedSecretNormalizedSource("red", NAMESPACE, true, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.default"); + Assertions.assertThat(sourceData.sourceData()).hasSize(1); + Assertions.assertThat(sourceData.sourceData().get("color")).isEqualTo("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).resource(pink).create(); + + NormalizedSource normalizedSource = new NamedSecretNormalizedSource("blue", NAMESPACE, true, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.blue.default"); + Assertions.assertThat(sourceData.sourceData()).isEmpty(); + } + + /** + * 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).resource(secret).create(); + + // different namespace + NormalizedSource normalizedSource = new NamedSecretNormalizedSource("red", NAMESPACE + "nope", true, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("color", "really-red")); + } + + /** + * we have two secrets deployed. one matches the query name. the other matches the + * active profile + name, thus is taken also. + */ + @Test + void matchIncludeSingleProfile() { + + Secret red = new SecretBuilder().withNewMetadata() + .withName("red") + .endMetadata() + .addToData("color", Base64.getEncoder().encodeToString("really-red".getBytes())) + .build(); + + Secret redWithProfile = new SecretBuilder().withNewMetadata() + .withName("red-with-profile") + .endMetadata() + .addToData("taste", Base64.getEncoder().encodeToString("mango".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(red).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(redWithProfile).create(); + + // add one more profile and specify that we want profile based config maps + MockEnvironment env = new MockEnvironment(); + env.setActiveProfiles("with-profile"); + NormalizedSource normalizedSource = new NamedSecretNormalizedSource("red", NAMESPACE, true, + ConfigUtils.Prefix.DEFAULT, true, true); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env, + NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.red-with-profile.default.with-profile"); + Assertions.assertThat(sourceData.sourceData()).hasSize(2); + Assertions.assertThat(sourceData.sourceData().get("color")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData().get("taste")).isEqualTo("mango"); + + } + + /** + * we have two secrets 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 + * secrets as well. + */ + @Test + void matchIncludeSingleProfileWithPrefix() { + + Secret red = new SecretBuilder().withNewMetadata() + .withName("red") + .endMetadata() + .addToData("color", Base64.getEncoder().encodeToString("really-red".getBytes())) + .build(); + + Secret redWithProfile = new SecretBuilder().withNewMetadata() + .withName("red-with-profile") + .endMetadata() + .addToData("taste", Base64.getEncoder().encodeToString("mango".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(red).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(redWithProfile).create(); + + // 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 NamedSecretNormalizedSource("red", NAMESPACE, true, PREFIX, true); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env, + NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.red-with-profile.default"); + Assertions.assertThat(sourceData.sourceData()).hasSize(2); + Assertions.assertThat(sourceData.sourceData().get("some.color")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData().get("some.taste")).isEqualTo("mango"); + + } + + /** + * we have three secrets 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() { + + Secret red = new SecretBuilder().withNewMetadata() + .withName("red") + .endMetadata() + .addToData("color", Base64.getEncoder().encodeToString("really-red".getBytes())) + .build(); + + Secret redWithTaste = new SecretBuilder().withNewMetadata() + .withName("red-with-taste") + .endMetadata() + .addToData("taste", Base64.getEncoder().encodeToString("mango".getBytes())) + .build(); + + Secret redWithShape = new SecretBuilder().withNewMetadata() + .withName("red-with-shape") + .endMetadata() + .addToData("shape", Base64.getEncoder().encodeToString("round".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(red).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(redWithTaste).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(redWithShape).create(); + + // 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 NamedSecretNormalizedSource("red", NAMESPACE, true, PREFIX, true); + + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env, + NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.red-with-shape.red-with-taste.default"); + + Assertions.assertThat(sourceData.sourceData()).hasSize(3); + Assertions.assertThat(sourceData.sourceData().get("some.color")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData().get("some.taste")).isEqualTo("mango"); + Assertions.assertThat(sourceData.sourceData().get("some.shape")).isEqualTo("round"); + + } + + /** + *
+	 *     - proves that single yaml file gets special treatment
+	 * 
+ */ + @Test + void testSingleYaml() { + Secret secret = new SecretBuilder().withNewMetadata() + .withName("single-yaml") + .endMetadata() + .addToData("single.yaml", Base64.getEncoder().encodeToString("key: value".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(secret).create(); + + // different namespace + NormalizedSource normalizedSource = new NamedSecretNormalizedSource("single-yaml", NAMESPACE, true, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), NAMESPACED_BATCH_READ); + + Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.single-yaml.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Collections.singletonMap("key", "value")); + } + + /** + *
+	 *     - two secrets are deployed : "red", "green", in the same namespace.
+	 *     - we first search for "red" and find it, and it is retrieved from the cluster via the client.
+	 *     - we then search for the "green" one, and it is not retrieved from the cache.
+	 * 
+ */ + @Test + void nonCache(CapturedOutput output) { + + Secret red = new SecretBuilder().withNewMetadata() + .withName("red") + .endMetadata() + .addToData("color", Base64.getEncoder().encodeToString("red".getBytes())) + .build(); + + Secret green = new SecretBuilder().withNewMetadata() + .withName("green") + .endMetadata() + .addToData("taste", Base64.getEncoder().encodeToString("mango".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(red).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(green).create(); + + MockEnvironment env = new MockEnvironment(); + NormalizedSource redNormalizedSource = new NamedSecretNormalizedSource("red", NAMESPACE, true, PREFIX, false); + Fabric8ConfigContext redContext = new Fabric8ConfigContext(mockClient, redNormalizedSource, NAMESPACE, env, + NAMESPACED_BATCH_READ); + Fabric8ContextToSourceData redData = new NamedSecretContextToSourceDataProvider().get(); + SourceData redSourceData = redData.apply(redContext); + + Assertions.assertThat(redSourceData.sourceName()).isEqualTo("secret.red.default"); + Assertions.assertThat(redSourceData.sourceData()).hasSize(1); + Assertions.assertThat(redSourceData.sourceData().get("some.color")).isEqualTo("red"); + + Assertions.assertThat(output.getAll()).doesNotContain("Loaded all secrets in namespace '" + NAMESPACE + "'"); + Assertions.assertThat(output.getOut()).contains("Will read individual secrets in namespace"); + + NormalizedSource greenNormalizedSource = new NamedSecretNormalizedSource("green", NAMESPACE, true, PREFIX, + false); + Fabric8ConfigContext greenContext = new Fabric8ConfigContext(mockClient, greenNormalizedSource, NAMESPACE, env, + NAMESPACED_BATCH_READ); + Fabric8ContextToSourceData greenData = new NamedSecretContextToSourceDataProvider().get(); + SourceData greenSourceData = greenData.apply(greenContext); + + Assertions.assertThat(greenSourceData.sourceName()).isEqualTo("secret.green.default"); + Assertions.assertThat(greenSourceData.sourceData()).hasSize(1); + Assertions.assertThat(greenSourceData.sourceData().get("some.taste")).isEqualTo("mango"); + + // meaning there is a single entry with such a log statement + String[] out = output.getAll().split("Loaded all secrets in namespace"); + Assertions.assertThat(out.length).isEqualTo(1); + + // meaning that the second read was done from the cache + out = output.getAll().split("Will read individual secrets in namespace"); + Assertions.assertThat(out.length).isEqualTo(3); + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/labeled_config_map_with_profile/LabeledConfigMapWithProfile.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/labeled_config_map_with_profile/LabeledConfigMapWithProfile.java index e716ccad95..9c582a4b78 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/labeled_config_map_with_profile/LabeledConfigMapWithProfile.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/labeled_config_map_with_profile/LabeledConfigMapWithProfile.java @@ -74,9 +74,6 @@ abstract class LabeledConfigMapWithProfile { * - configmap with name "color-configmap-k8s", with labels : "{color: not-blue}" * - configmap with name "green-configmap-k8s", with labels : "{color: green-k8s}" * - configmap with name "green-configmap-prod", with labels : "{color: green-prod}" - * - * # a test that proves order: first read non-profile based configmaps, thus profile based - * # configmaps override non-profile ones. * - configmap with name "green-purple-configmap", labels "{color: green, shape: round}", data: "{eight: 8}" * - configmap with name "green-purple-configmap-k8s", labels "{color: black}", data: "{eight: eight-ish}" * @@ -95,7 +92,7 @@ static void setUpBeforeClass(KubernetesClient mockClient) { Map colorConfigMap = Collections.singletonMap("one", "1"); createConfigMap("color-configmap", colorConfigMap, Collections.singletonMap("color", "blue")); - // is not taken, since "profileSpecificSources=false" for the above + // is not taken Map colorConfigMapK8s = Collections.singletonMap("five", "5"); createConfigMap("color-configmap-k8s", colorConfigMapK8s, Collections.singletonMap("color", "not-blue")); @@ -103,13 +100,13 @@ static void setUpBeforeClass(KubernetesClient mockClient) { Map greenConfigMap = Collections.singletonMap("two", "2"); createConfigMap("green-configmap", greenConfigMap, Collections.singletonMap("color", "green")); - // is taken because k8s profile is active and "profileSpecificSources=true" + // is taken Map greenConfigMapK8s = Collections.singletonMap("six", "6"); - createConfigMap("green-configmap-k8s", greenConfigMapK8s, Collections.singletonMap("color", "green-k8s")); + createConfigMap("green-configmap-k8s", greenConfigMapK8s, Collections.singletonMap("color", "green")); - // is taken because prod profile is active and "profileSpecificSources=true" + // is taken Map greenConfigMapProd = Collections.singletonMap("seven", "7"); - createConfigMap("green-configmap-prod", greenConfigMapProd, Collections.singletonMap("color", "green-prod")); + createConfigMap("green-configmap-prod", greenConfigMapProd, Collections.singletonMap("color", "green")); // not taken Map redConfigMap = Collections.singletonMap("three", "3"); @@ -125,7 +122,7 @@ static void setUpBeforeClass(KubernetesClient mockClient) { // is taken and thus overrides the above Map greenPurpleK8s = Collections.singletonMap("eight", "eight-ish"); - createConfigMap("green-purple-configmap-k8s", greenPurpleK8s, Map.of("color", "black")); + createConfigMap("green-purple-configmap-k8s", greenPurpleK8s, Map.of("color", "green")); } @@ -144,7 +141,7 @@ private static void createConfigMap(String name, Map data, Map * this one is taken from : "blue.one". We find "color-configmap" by labels, and - * "color-configmap-k8s" exists, but "includeProfileSpecificSources=false", thus not taken. + * "color-configmap-k8s" exists, but not taken. * Since "explicitPrefix=blue", we take "blue.one" * */ @@ -154,7 +151,11 @@ void testBlue() { } /** - * found by labels. + * <<<<<<< HEAD
+	 *   this one is taken from : "green-configmap.green-configmap-k8s.green-configmap-prod.green-purple-configmap.green-purple-configmap-k8s".
+	 *   We find "green-configmap", "green-configmap-k8s", "green-configmap-prod"  by labels.
+	 *   Also "green-purple-configmap" and "green-purple-configmap-k8s" are found.
+	 * 
======= found by labels. >>>>>>> main */ @Test void testGreen() { diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/labeled_secret_with_profile/LabeledSecretWithProfile.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/labeled_secret_with_profile/LabeledSecretWithProfile.java index 5f6f9dd4a3..b73090df7f 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/labeled_secret_with_profile/LabeledSecretWithProfile.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/labeled_secret_with_profile/LabeledSecretWithProfile.java @@ -75,9 +75,12 @@ abstract class LabeledSecretWithProfile { * - secret with name "color-secret-k8s", with labels : "{color: not-blue}" * - secret with name "green-secret-k8s", with labels : "{color: green-k8s}" * - secret with name "green-secret-prod", with labels : "{color: green-prod}" + <<<<<<< HEAD + ======= * * a test that proves order: first read non-profile based secrets, thus profile based * secrets override non-profile ones. + >>>>>>> main * - secret with name "green-purple-secret", labels "{color: green, shape: round}", data: "{eight: 8}" * - secret with name "green-purple-secret-k8s", labels "{color: black}", data: "{eight: eight-ish}" * @@ -97,7 +100,7 @@ static void setUpBeforeClass(KubernetesClient mockClient) { Base64.getEncoder().encodeToString("1".getBytes(StandardCharsets.UTF_8))); createSecret("color-secret", colorSecret, Collections.singletonMap("color", "blue")); - // is not taken, since "profileSpecificSources=false" for the above + // is not taken Map colorSecretK8s = Collections.singletonMap("five", Base64.getEncoder().encodeToString("5".getBytes(StandardCharsets.UTF_8))); createSecret("color-secret-k8s", colorSecretK8s, Collections.singletonMap("color", "not-blue")); @@ -107,15 +110,15 @@ static void setUpBeforeClass(KubernetesClient mockClient) { Base64.getEncoder().encodeToString("2".getBytes(StandardCharsets.UTF_8))); createSecret("green-secret", greenSecret, Collections.singletonMap("color", "green")); - // is taken because k8s profile is active and "profileSpecificSources=true" + // is taken Map shapeSecretK8s = Collections.singletonMap("six", Base64.getEncoder().encodeToString("6".getBytes(StandardCharsets.UTF_8))); - createSecret("green-secret-k8s", shapeSecretK8s, Collections.singletonMap("color", "green-k8s")); + createSecret("green-secret-k8s", shapeSecretK8s, Collections.singletonMap("color", "green")); - // // is taken because prod profile is active and "profileSpecificSources=true" + // // is taken Map shapeSecretProd = Collections.singletonMap("seven", Base64.getEncoder().encodeToString("7".getBytes(StandardCharsets.UTF_8))); - createSecret("green-secret-prod", shapeSecretProd, Collections.singletonMap("color", "green-prod")); + createSecret("green-secret-prod", shapeSecretProd, Collections.singletonMap("color", "green")); // not taken Map redSecret = Collections.singletonMap("three", @@ -135,7 +138,7 @@ static void setUpBeforeClass(KubernetesClient mockClient) { // is taken and thus overrides the above Map greenPurpleK8s = Collections.singletonMap("eight", Base64.getEncoder().encodeToString("eight-ish".getBytes(StandardCharsets.UTF_8))); - createSecret("green-purple-secret-k8s", greenPurpleK8s, Map.of("color", "black")); + createSecret("green-purple-secret-k8s", greenPurpleK8s, Map.of("color", "green")); } @@ -154,7 +157,7 @@ private static void createSecret(String name, Map data, Map * this one is taken from : "blue.one". We find "color-secret" by labels, and - * "color-secrets-k8s" exists, but "includeProfileSpecificSources=false", thus not taken. + * "color-secrets-k8s". * Since "explicitPrefix=blue", we take "blue.one" * */ @@ -165,7 +168,13 @@ void testBlue() { /** *
+	<<<<<<< HEAD
+	 *   this one is taken from : "green-purple-secret.green-purple-secret-k8s.green-secret.green-secret-k8s.green-secret-prod".
+	 *   We find "green-secret", "green-secrets-k8s" and "green-secrets-prod" by labels.
+	 *   Also "green-purple-secret" and "green-purple-secret-k8s" are found.
+	=======
 	 *   We find "green-secret" by labels.
+	>>>>>>> main
 	 * 
*/ @Test diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/locator_retry/fail_fast_enabled_retry_disabled/ConfigDataConfigFailFastEnabledButRetryDisabledTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/locator_retry/fail_fast_enabled_retry_disabled/ConfigDataConfigFailFastEnabledButRetryDisabledTests.java index 985e0cc9ff..3059aa85b3 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/locator_retry/fail_fast_enabled_retry_disabled/ConfigDataConfigFailFastEnabledButRetryDisabledTests.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/locator_retry/fail_fast_enabled_retry_disabled/ConfigDataConfigFailFastEnabledButRetryDisabledTests.java @@ -73,7 +73,7 @@ static class LocalConfig { ConfigMapConfigProperties properties(Environment environment) { return new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, null, null, false, true, Boolean.parseBoolean(environment.getProperty("spring.cloud.kubernetes.config.fail-fast")), - RetryProperties.DEFAULT); + RetryProperties.DEFAULT, true); } } diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadConfigMapTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadConfigMapTest.java index 9702dd6572..de18463f7b 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadConfigMapTest.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadConfigMapTest.java @@ -162,7 +162,8 @@ AbstractEnvironment environment() { // simulate that environment already has a Fabric8ConfigMapPropertySource, // otherwise we can't properly test reload functionality ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), - List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT); + List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT, + false); KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); PropertySource propertySource = new VisibleFabric8ConfigMapPropertySourceLocator(kubernetesClient, @@ -185,7 +186,7 @@ ConfigReloadProperties configReloadProperties() { @Primary ConfigMapConfigProperties configMapConfigProperties() { return new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, - false, true, FAIL_FAST, RetryProperties.DEFAULT); + false, true, FAIL_FAST, RetryProperties.DEFAULT, false); } @Bean diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadSecretTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadSecretTest.java index 7515bba3f8..884e0699ae 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadSecretTest.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadSecretTest.java @@ -169,7 +169,7 @@ AbstractEnvironment environment() { // Fabric8SecretsPropertySourceLocator, // otherwise we can't properly test reload functionality SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), - List.of(), true, SECRET_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT); + List.of(), true, SECRET_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT, false); KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); PropertySource propertySource = new VisibleFabric8SecretsPropertySourceLocator(kubernetesClient, @@ -192,7 +192,7 @@ ConfigReloadProperties configReloadProperties() { @Primary SecretsConfigProperties secretsConfigProperties() { return new SecretsConfigProperties(true, Map.of(), List.of(), List.of(), true, SECRET_NAME, NAMESPACE, - false, true, FAIL_FAST, RetryProperties.DEFAULT); + false, true, FAIL_FAST, RetryProperties.DEFAULT, false); } @Bean diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadConfigMapTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadConfigMapTest.java index d59e4cb85a..6a881fcaf3 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadConfigMapTest.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadConfigMapTest.java @@ -152,7 +152,8 @@ AbstractEnvironment environment() { // simulate that environment already has a Fabric8ConfigMapPropertySource, // otherwise we can't properly test reload functionality ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), - List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT); + List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT, + true); KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); PropertySource propertySource = new VisibleFabric8ConfigMapPropertySourceLocator(kubernetesClient, @@ -175,7 +176,7 @@ ConfigReloadProperties configReloadProperties() { @Primary ConfigMapConfigProperties configMapConfigProperties() { return new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, - false, true, FAIL_FAST, RetryProperties.DEFAULT); + false, true, FAIL_FAST, RetryProperties.DEFAULT, true); } @Bean diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadSecretTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadSecretTest.java index c9bb1eba40..1660abd7dc 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadSecretTest.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadSecretTest.java @@ -159,7 +159,7 @@ AbstractEnvironment environment() { // simulate that environment already has a Fabric8SecretsPropertySource, // otherwise we can't properly test reload functionality SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), - List.of(), true, SECRET_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT); + List.of(), true, SECRET_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT, true); KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); PropertySource propertySource = new VisibleFabric8SecretsPropertySourceLocator(kubernetesClient, @@ -182,7 +182,7 @@ ConfigReloadProperties configReloadProperties() { @Primary SecretsConfigProperties secretsConfigProperties() { return new SecretsConfigProperties(true, Map.of(), List.of(), List.of(), true, SECRET_NAME, NAMESPACE, - false, true, FAIL_FAST, RetryProperties.DEFAULT); + false, true, FAIL_FAST, RetryProperties.DEFAULT, true); } @Bean diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/resources/labeled-configmap-with-profile.yaml b/spring-cloud-kubernetes-fabric8-config/src/test/resources/labeled-configmap-with-profile.yaml index 8aa88d2deb..5cde6f0742 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/resources/labeled-configmap-with-profile.yaml +++ b/spring-cloud-kubernetes-fabric8-config/src/test/resources/labeled-configmap-with-profile.yaml @@ -7,12 +7,10 @@ spring: enableApi: true useNameAsPrefix: true namespace: spring-k8s - includeProfileSpecificSources: true sources: - labels: color: blue explicitPrefix: blue - includeProfileSpecificSources: false - labels: color: green - labels: diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/resources/labeled-secret-with-profile.yaml b/spring-cloud-kubernetes-fabric8-config/src/test/resources/labeled-secret-with-profile.yaml index 89255f0810..b22cac4bf9 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/resources/labeled-secret-with-profile.yaml +++ b/spring-cloud-kubernetes-fabric8-config/src/test/resources/labeled-secret-with-profile.yaml @@ -7,12 +7,10 @@ spring: enableApi: true useNameAsPrefix: true namespace: spring-k8s - includeProfileSpecificSources: true sources: - labels: color: blue explicitPrefix: blue - includeProfileSpecificSources: false - labels: color: green - labels: