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 0af000241..74ca27d41 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 19d7f14ff..3dbf5f70e 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 56a17e6a7..9015db29a 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 21bc92b9e..09b99a2f0 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 configmaps 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 49df6f830..000000000 --- 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 057a43d90..d9440b425 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 3a0d046db..6fb8d35dd 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 000000000..266531544 --- /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 000000000..8ac6e78da --- /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 6d4381ffd..98b26edd5 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; @@ -49,13 +48,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 9cf72cbc0..a20905aef 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 56645a4c5..570979193 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 bfde14006..c77c42cc2 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/KubernetesClientConfigMapPropertySourceLocatorTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocatorTests.java index 1266b00f7..6e2204d17 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 @@ -57,6 +57,8 @@ */ 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") @@ -100,7 +102,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, @@ -119,7 +122,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())) @@ -142,7 +146,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())) @@ -162,7 +167,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); @@ -175,7 +181,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())); @@ -191,7 +198,8 @@ public void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() { .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 51916d24c..df63c7dbc 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 b9ecb61cc..c7a2ecb91 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 @@ -52,6 +52,8 @@ */ 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 = "{\n" + "\t\"kind\": \"SecretList\",\n" + "\t\"apiVersion\": \"v1\",\n" @@ -115,7 +117,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) @@ -129,7 +132,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) @@ -151,7 +155,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) @@ -164,7 +168,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); @@ -179,7 +184,8 @@ void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() { 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 4cc2b8d23..3b87f5595 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() @@ -126,7 +128,7 @@ static void after() { @AfterEach void afterEach() { WireMock.reset(); - new KubernetesClientSecretsCache().discardAll(); + new KubernetesClientSourcesNamespaceBatched().discardSecrets(); } @Test @@ -137,7 +139,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"); @@ -151,7 +153,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(); @@ -167,9 +169,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(); @@ -183,7 +185,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) @@ -198,7 +200,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 44840858e..f63fa7caf 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.V1ConfigMapList; import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; import io.kubernetes.client.util.ClientBuilder; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; @@ -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); @@ -356,7 +364,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); @@ -395,7 +403,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); @@ -408,9 +416,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() { @@ -425,7 +432,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") @@ -436,11 +443,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); @@ -490,7 +497,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") @@ -498,7 +505,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") @@ -513,11 +520,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); @@ -572,19 +579,21 @@ 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); Assertions.assertEquals(redSourceData.sourceData().size(), 1); Assertions.assertEquals(redSourceData.sourceData().get("color"), "red"); Assertions.assertEquals(redSourceData.sourceName(), "configmap.red-configmap.default"); + Assertions.assertTrue(output.getAll().contains("Loaded all config maps in namespace '" + NAMESPACE + "'")); + Assertions.assertFalse(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()); + 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 000000000..fa844f6f4 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/LabeledConfigMapContextToSourceDataProviderNonNamespacedBatchReadTests.java @@ -0,0 +1,630 @@ +/* + * 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.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.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.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.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.assertEquals("configmap.test-configmap.default", sourceData.sourceName()); + Assertions.assertEquals(Map.of("name", "value"), sourceData.sourceData()); + + } + + /** + * 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.assertEquals(sourceData.sourceName(), "configmap.red-configmap.red-configmap-again.default"); + Assertions.assertEquals(sourceData.sourceData().size(), 2); + Assertions.assertEquals(sourceData.sourceData().get("colorOne"), "really-red"); + Assertions.assertEquals(sourceData.sourceData().get("colorTwo"), "really-red-again"); + + } + + /** + * one 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.assertEquals(sourceData.sourceName(), "configmap.color.default"); + Assertions.assertEquals(sourceData.sourceData(), Collections.emptyMap()); + + } + + /** + * 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.assertEquals("configmap.test-configmap.default", sourceData.sourceName()); + Assertions.assertEquals(Map.of("name", "value"), sourceData.sourceData()); + } + + /** + * 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.assertEquals("configmap.blue-configmap.default", sourceData.sourceName()); + Assertions.assertEquals(Map.of("me.what-color", "blue-color"), sourceData.sourceData()); + } + + /** + * 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.assertEquals(sourceData.sourceName(), "configmap.another-blue-configmap.blue-configmap.default"); + + Map properties = sourceData.sourceData(); + Assertions.assertEquals(2, properties.size()); + Iterator keys = properties.keySet().iterator(); + String firstKey = keys.next(); + String secondKey = keys.next(); + + if (firstKey.contains("first")) { + Assertions.assertEquals(firstKey, "another-blue-configmap.blue-configmap.first"); + } + + Assertions.assertEquals(secondKey, "another-blue-configmap.blue-configmap.second"); + Assertions.assertEquals(properties.get(firstKey), "blue"); + Assertions.assertEquals(properties.get(secondKey), "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.assertTrue(sourceData.sourceData().isEmpty()); + Assertions.assertEquals(sourceData.sourceName(), "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.assertEquals(sourceData.sourceData().size(), 1); + Assertions.assertEquals(sourceData.sourceData().get("one"), "1"); + Assertions.assertEquals(sourceData.sourceName(), "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.assertEquals(sourceData.sourceData().size(), 2); + Assertions.assertEquals(sourceData.sourceData().get("color-configmap.color-configmap-k8s.one"), "1"); + Assertions.assertEquals(sourceData.sourceData().get("color-configmap.color-configmap-k8s.two"), "2"); + Assertions.assertEquals(sourceData.sourceName(), "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.assertEquals(sourceData.sourceData().size(), 4); + Assertions.assertEquals(sourceData.sourceData() + .get("color-configmap.color-configmap-k8s.shape-configmap.shape-configmap-k8s.one"), "1"); + Assertions.assertEquals(sourceData.sourceData() + .get("color-configmap.color-configmap-k8s.shape-configmap.shape-configmap-k8s.two"), "2"); + Assertions.assertEquals(sourceData.sourceData() + .get("color-configmap.color-configmap-k8s.shape-configmap.shape-configmap-k8s.four"), "4"); + Assertions.assertEquals(sourceData.sourceData() + .get("color-configmap.color-configmap-k8s.shape-configmap.shape-configmap-k8s.five"), "5"); + + Assertions.assertEquals(sourceData.sourceName(), + "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.assertEquals(redSourceData.sourceData().size(), 1); + Assertions.assertEquals(redSourceData.sourceData().get("color"), "red"); + Assertions.assertEquals(redSourceData.sourceName(), "configmap.red-configmap.default"); + + Assertions.assertFalse(output.getAll().contains("Loaded all config maps in namespace '" + NAMESPACE + "'")); + Assertions.assertTrue(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.assertEquals(greenSourceData.sourceData().size(), 1); + Assertions.assertEquals(greenSourceData.sourceData().get("color"), "green"); + Assertions.assertEquals(greenSourceData.sourceName(), "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.assertEquals(out.length, 1); + + // meaning that the second read was done from the cache + out = output.getAll().split("Will read individual configmaps in namespace"); + Assertions.assertEquals(out.length, 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 91% 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 9a666509c..fc6522419 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. @@ -33,6 +33,7 @@ import io.kubernetes.client.openapi.models.V1SecretBuilder; import io.kubernetes.client.openapi.models.V1SecretList; import io.kubernetes.client.util.ClientBuilder; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; @@ -56,7 +57,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<>(); @@ -84,7 +87,12 @@ static void setup() { @AfterEach void afterEach() { WireMock.reset(); - new KubernetesClientSecretsCache().discardAll(); + new KubernetesClientSourcesNamespaceBatched().discardSecrets(); + } + + @AfterAll + static void afterAll() { + WireMock.shutdownServer(); } /** @@ -107,9 +115,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); @@ -135,9 +143,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); @@ -167,9 +175,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); @@ -192,9 +200,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); @@ -225,10 +233,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); @@ -271,9 +278,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); @@ -303,7 +310,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() { @@ -330,9 +337,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); @@ -345,9 +352,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() { @@ -361,7 +367,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()) @@ -373,11 +379,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); @@ -426,7 +432,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()) @@ -434,7 +440,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()) @@ -450,11 +456,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); @@ -493,9 +499,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); @@ -538,21 +544,23 @@ 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); Assertions.assertEquals(redSourceData.sourceData().size(), 1); Assertions.assertEquals(redSourceData.sourceData().get("color"), "red"); Assertions.assertEquals(redSourceData.sourceName(), "secret.red.default"); + Assertions.assertTrue(output.getAll().contains("Loaded all secrets in namespace '" + NAMESPACE + "'")); + Assertions.assertFalse(output.getAll().contains("Will read individual secrets in 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 000000000..2c4172f6c --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/LabeledSecretContextToSourceDataProviderNonNamespacedBatchReadTests.java @@ -0,0 +1,588 @@ +/* + * 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.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.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.assertEquals(sourceData.sourceName(), "secret.color.default"); + Assertions.assertEquals(sourceData.sourceData(), Collections.emptyMap()); + + } + + /** + * we have a single secret deployed. it has two labels and these match against our + * queries. + */ + @Test + void singleSecretMatchAgainstLabels() { + + 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.assertEquals(sourceData.sourceName(), "secret.test-secret.default"); + Assertions.assertEquals(sourceData.sourceData(), Map.of("color", "really-red")); + + } + + /** + * we have two secrets deployed. both of them have labels that match (color=red). + */ + @Test + void twoSecretsMatchAgainstLabels() { + + 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.assertEquals(sourceData.sourceName(), "secret.color-one.color-two.default"); + Assertions.assertEquals(sourceData.sourceData().size(), 2); + Assertions.assertEquals(sourceData.sourceData().get("colorOne"), "really-red-one"); + Assertions.assertEquals(sourceData.sourceData().get("colorTwo"), "really-red-two"); + + } + + @Test + void namespaceMatch() { + 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.assertEquals(sourceData.sourceName(), "secret.test-secret.default"); + Assertions.assertEquals(sourceData.sourceData(), 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.assertEquals("secret.blue-secret.default", sourceData.sourceName()); + Assertions.assertEquals(Map.of("me.what-color", "blue-color"), sourceData.sourceData()); + } + + /** + * 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.assertEquals(46, sourceData.sourceName().length()); + Assertions.assertTrue(sourceData.sourceName().contains("secret")); + Assertions.assertTrue(sourceData.sourceName().contains("blue-secret")); + Assertions.assertTrue(sourceData.sourceName().contains("another-blue-secret")); + Assertions.assertTrue(sourceData.sourceName().contains("default")); + + Map properties = sourceData.sourceData(); + Assertions.assertEquals(2, properties.size()); + Iterator keys = properties.keySet().iterator(); + String firstKey = keys.next(); + String secondKey = keys.next(); + + if (firstKey.contains("first")) { + Assertions.assertEquals(firstKey, "another-blue-secret.blue-secret.first"); + } + + Assertions.assertEquals(secondKey, "another-blue-secret.blue-secret.second"); + Assertions.assertEquals(properties.get(firstKey), "blue"); + Assertions.assertEquals(properties.get(secondKey), "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.assertEquals(sourceData.sourceData().size(), 1); + Assertions.assertEquals(sourceData.sourceData().get("one"), "1"); + Assertions.assertEquals(sourceData.sourceName(), "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("color-secret") + .build()) + .addToData("one", "1".getBytes()) + .build(); + + V1Secret shapeSecret = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue")) + .withNamespace(NAMESPACE) + .withName("color-secret-k8s") + .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.assertEquals(sourceData.sourceData().size(), 2); + Assertions.assertEquals(sourceData.sourceData().get("color-secret.color-secret-k8s.one"), "1"); + Assertions.assertEquals(sourceData.sourceData().get("color-secret.color-secret-k8s.two"), "2"); + Assertions.assertEquals(sourceData.sourceName(), "secret.color-secret.color-secret-k8s.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.assertEquals(sourceData.sourceData().size(), 4); + Assertions.assertEquals( + sourceData.sourceData().get("color-secret.color-secret-k8s.shape-secret.shape-secret-k8s.one"), "1"); + Assertions.assertEquals( + sourceData.sourceData().get("color-secret.color-secret-k8s.shape-secret.shape-secret-k8s.two"), "2"); + Assertions.assertEquals( + sourceData.sourceData().get("color-secret.color-secret-k8s.shape-secret.shape-secret-k8s.four"), "4"); + Assertions.assertEquals( + sourceData.sourceData().get("color-secret.color-secret-k8s.shape-secret.shape-secret-k8s.five"), "5"); + + Assertions.assertEquals(sourceData.sourceName(), + "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.assertEquals(sourceData.sourceData().size(), 1); + Assertions.assertEquals(sourceData.sourceData().get("color"), "blue"); + Assertions.assertEquals(sourceData.sourceName(), "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.assertEquals(redSourceData.sourceData().size(), 1); + Assertions.assertEquals(redSourceData.sourceData().get("color"), "red"); + Assertions.assertEquals(redSourceData.sourceName(), "secret.red.default"); + + Assertions.assertFalse(output.getAll().contains("Loaded all secrets in namespace '" + NAMESPACE + "'")); + Assertions.assertTrue(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.assertEquals(greenSourceData.sourceData().size(), 1); + Assertions.assertEquals(greenSourceData.sourceData().get("color"), "green"); + Assertions.assertEquals(greenSourceData.sourceName(), "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.assertEquals(out.length, 1); + + // meaning that the second read was done from the cache + out = output.getAll().split("Will read individual secrets in namespace"); + Assertions.assertEquals(out.length, 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 dcdbda388..243b2c34e 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. @@ -30,6 +30,7 @@ import io.kubernetes.client.openapi.models.V1ConfigMapList; import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; import io.kubernetes.client.util.ClientBuilder; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; @@ -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,7 +179,7 @@ void matchIncludeSingleProfile() { MockEnvironment environment = new MockEnvironment(); environment.setActiveProfiles("with-profile"); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, - false); + false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -215,7 +221,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 +277,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 +312,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 +343,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 +370,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 +402,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,17 +442,19 @@ 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); Assertions.assertEquals(redSourceData.sourceName(), "configmap.red.default"); Assertions.assertEquals(redSourceData.sourceData(), Map.of("color", "red")); + Assertions.assertTrue(output.getAll().contains("Loaded all config maps in namespace '" + NAMESPACE + "'")); + Assertions.assertFalse(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); + 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 000000000..762d093b4 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/NamedConfigMapContextToSourceDataProviderNonNamespacedBatchReadTests.java @@ -0,0 +1,466 @@ +/* + * 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.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.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.assertEquals(sourceData.sourceName(), "configmap.blue.default"); + Assertions.assertEquals(sourceData.sourceData(), Map.of()); + + } + + /** + *
+	 *     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.assertEquals(sourceData.sourceName(), "configmap.red.default"); + Assertions.assertEquals(sourceData.sourceData(), 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.assertEquals(sourceData.sourceName(), "configmap.red.red-with-profile.default.with-profile"); + Assertions.assertEquals(sourceData.sourceData().size(), 1); + Assertions.assertEquals(sourceData.sourceData().get("taste"), "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.assertEquals(sourceData.sourceName(), "configmap.red.red-with-profile.default"); + Assertions.assertEquals(sourceData.sourceData().size(), 2); + Assertions.assertEquals(sourceData.sourceData().get("some.color"), "really-red"); + Assertions.assertEquals(sourceData.sourceData().get("some.taste"), "mango"); + + } + + /** + *
+	 *     - 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.assertEquals(sourceData.sourceName(), "configmap.red.red-with-shape.red-with-taste.default"); + Assertions.assertEquals(sourceData.sourceData().size(), 3); + Assertions.assertEquals(sourceData.sourceData().get("some.color"), "really-red"); + Assertions.assertEquals(sourceData.sourceData().get("some.taste"), "mango"); + Assertions.assertEquals(sourceData.sourceData().get("some.shape"), "round"); + + } + + /** + *
+	 * 		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.assertEquals(sourceData.sourceName(), "configmap.application.default"); + Assertions.assertEquals(sourceData.sourceData(), Map.of()); + } + + /** + *
+	 *     - 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.assertEquals(sourceData.sourceName(), "configmap.red.default"); + Assertions.assertEquals(sourceData.sourceData(), 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.assertEquals(sourceData.sourceName(), "configmap.red.default"); + Assertions.assertEquals(sourceData.sourceData(), 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.assertEquals(sourceData.sourceName(), "configmap.one.default"); + Assertions.assertEquals(sourceData.sourceData(), 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.assertEquals(redSourceData.sourceName(), "configmap.red.default"); + Assertions.assertEquals(redSourceData.sourceData(), Map.of("color", "red")); + + Assertions.assertFalse(output.getAll().contains("Loaded all config maps in namespace '" + NAMESPACE + "'")); + Assertions.assertTrue(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.assertEquals(greenSourceData.sourceName(), "configmap.green.default"); + Assertions.assertEquals(greenSourceData.sourceData(), 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.assertEquals(out.length, 1); + + // meaning that the second read was done from the cache + out = output.getAll().split("Will read individual configmaps in namespace"); + Assertions.assertEquals(out.length, 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 e78b2426d..a72279c7f 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.V1SecretList; import io.kubernetes.client.openapi.models.V1SecretListBuilder; import io.kubernetes.client.util.ClientBuilder; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; @@ -51,7 +52,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"); @@ -74,7 +77,12 @@ static void setup() { @AfterEach void afterEach() { WireMock.reset(); - new KubernetesClientSecretsCache().discardAll(); + new KubernetesClientSourcesNamespaceBatched().discardSecrets(); + } + + @AfterAll + static void afterAll() { + WireMock.shutdownServer(); } /** @@ -94,7 +102,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); @@ -134,7 +142,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); @@ -163,7 +171,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); @@ -194,7 +202,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); @@ -230,7 +238,7 @@ void matchIncludeSingleProfile() { MockEnvironment environment = new MockEnvironment(); environment.addActiveProfile("with-profile"); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, - false); + false, NAMESPACED_BATCH_READ); KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -268,7 +276,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 +321,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 +354,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 +393,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 +403,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 000000000..760ebd2ef --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/NamedSecretContextToSourceDataProviderNonNamespacedBatchReadTests.java @@ -0,0 +1,420 @@ +/* + * 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.Collections; +import java.util.Map; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.Configuration; +import io.kubernetes.client.openapi.JSON; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.openapi.models.V1Secret; +import io.kubernetes.client.openapi.models.V1SecretBuilder; +import io.kubernetes.client.util.ClientBuilder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.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.assertEquals(sourceData.sourceName(), "secret.red.default"); + Assertions.assertEquals(sourceData.sourceData(), Map.of("color", "really-red")); + + } + + /** + * we have three secrets deployed. one of them has a name that matches (red), the + * other two have different names, thus no match. + */ + @Test + void twoSecretMatchAgainstLabels() { + + 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.assertEquals(sourceData.sourceName(), "secret.red.default"); + Assertions.assertEquals(sourceData.sourceData().size(), 1); + Assertions.assertEquals(sourceData.sourceData().get("color"), "really-red"); + + } + + /** + * one secret deployed (pink), does not match our query (blue). + */ + @Test + void testSecretNoMatch() { + + 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.assertEquals(sourceData.sourceName(), "secret.blue.default"); + Assertions.assertEquals(sourceData.sourceData(), Collections.emptyMap()); + } + + /** + *
+	 *     - 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.assertEquals(sourceData.sourceName(), "secret.red.default"); + Assertions.assertEquals(sourceData.sourceData(), 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.assertEquals(sourceData.sourceName(), "secret.red.red-with-profile.default.with-profile"); + Assertions.assertEquals(sourceData.sourceData().size(), 1); + Assertions.assertEquals(sourceData.sourceData().get("taste"), "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.assertEquals(sourceData.sourceName(), "secret.red.red-with-taste.default"); + Assertions.assertEquals(sourceData.sourceData().size(), 2); + Assertions.assertEquals(sourceData.sourceData().get("some.color"), "really-red"); + Assertions.assertEquals(sourceData.sourceData().get("some.taste"), "mango"); + + } + + /** + * we have three 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.assertEquals(sourceData.sourceName(), "secret.red.red-with-shape.red-with-taste.default"); + + Assertions.assertEquals(sourceData.sourceData().size(), 3); + Assertions.assertEquals(sourceData.sourceData().get("some.color"), "really-red"); + Assertions.assertEquals(sourceData.sourceData().get("some.taste"), "mango"); + Assertions.assertEquals(sourceData.sourceData().get("some.shape"), "round"); + + } + + /** + *
+	 *     - 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.assertEquals(sourceData.sourceName(), "secret.single-yaml.default"); + Assertions.assertEquals(sourceData.sourceData(), 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.assertEquals(redSourceData.sourceName(), "secret.red.default"); + Assertions.assertEquals(redSourceData.sourceData(), Map.of("color", "red")); + + Assertions.assertFalse(output.getAll().contains("Loaded all secrets in namespace '" + NAMESPACE + "'")); + Assertions.assertTrue(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.assertEquals(greenSourceData.sourceName(), "secret.green.default"); + Assertions.assertEquals(greenSourceData.sourceData(), 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.assertEquals(out.length, 1); + + // meaning that the second read was done from the cache + out = output.getAll().split("Will read individual secrets in namespace"); + Assertions.assertEquals(out.length, 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_config_map_with_profile/LabeledConfigMapWithProfileTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_config_map_with_profile/LabeledConfigMapWithProfileTests.java index cc4b6798b..b5d4422a1 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_config_map_with_profile/LabeledConfigMapWithProfileTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/applications/labeled_config_map_with_profile/LabeledConfigMapWithProfileTests.java @@ -66,10 +66,8 @@ void testBlue() { /** *
-	 *   this one is taken from : ""green-configmap.green-configmap-k8s.green-configmap-prod.green-purple-configmap.green-purple-configmap-k8s"".
-	 *   We find "green-configmap" by labels, also "green-configmap-k8s", "green-configmap-prod" exists,
-	 *   because "includeProfileSpecificSources=true" is set. Also "green-purple-configmap" and "green-purple-configmap-k8s"
-	 *   are found.
+	 *   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.
 	 * 
*/ @Test 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 b66dab4a1..fc409b2ce 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 @@ -60,8 +60,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"
 	 * 
*/ @@ -79,9 +78,7 @@ void testBlue() { /** *
 	 *   this one is taken from : "green-purple-secret.green-purple-secret-k8s.green-secret.green-secret-k8s.green-secret-prod".
-	 *   We find "green-secret" by labels, also "green-secrets-k8s" and "green-secrets-prod" exists,
-	 *   because "includeProfileSpecificSources=true" is set. Also "green-purple-secret" and "green-purple-secret-k8s"
-	 * 	 are found.
+	 *   We find "green-secret", "green-secrets-k8s" and "green-secrets-prod" by labels.
 	 * 
*/ @Test 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 3f43f594e..5798872aa 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 8e1ed6120..3133bd27e 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 c59ca39f1..072bc291f 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-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 6bb155bca..90a7afe58 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 d0c70e72f..2e14df8a3 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 8c5572466..5ffae1b33 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 0fb4f504c..424170706 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 1ca300b5b..3d0978bcc 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 @@ -71,7 +71,7 @@ public ConfigMapPropertySourceLocator(ConfigMapConfigProperties properties, Conf } protected abstract MapPropertySource getMapPropertySource(NormalizedSource normalizedSource, - ConfigurableEnvironment environment); + ConfigurableEnvironment environment, boolean namespacedBatchRead); @Override public PropertySource locate(Environment environment) { @@ -81,12 +81,13 @@ public PropertySource locate(Environment environment) { if (this.properties.enableApi()) { Set sources = new LinkedHashSet<>(this.properties.determineSources(environment)); LOG.debug("Config Map normalized sources : " + sources); - sources.forEach(s -> composite.addFirstPropertySource(getMapPropertySource(s, env))); + sources.forEach(s -> composite + .addFirstPropertySource(getMapPropertySource(s, env, properties.namespacedBatchRead()))); } 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 5f40858ea..f89d881ee 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; @@ -181,6 +179,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); } @@ -193,6 +194,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())); @@ -216,9 +221,10 @@ public static MultipleSourcesContainer processNamedData(List activeProf /** * 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 result = 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 aecd0f35f..4f32097c0 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 df4e6aab1..69e524415 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,9 +16,7 @@ package org.springframework.cloud.kubernetes.commons.config; -import java.util.Arrays; import java.util.Map; -import java.util.Set; import java.util.stream.Collectors; import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.onException; @@ -33,16 +31,12 @@ public abstract class LabeledSourceData { public final SourceData compute(Map labels, ConfigUtils.Prefix prefix, String target, - boolean profileSources, boolean failFast, String namespace, String[] activeProfiles) { + boolean failFast, String namespace) { MultipleSourcesContainer data = MultipleSourcesContainer.empty(); try { - Set profiles = Set.of(); - if (profileSources) { - profiles = Arrays.stream(activeProfiles).collect(Collectors.toSet()); - } - data = dataSupplier(labels, profiles); + data = dataSupplier(labels); // need this check because when there is no data, the name of the property // source @@ -86,11 +80,9 @@ public final SourceData compute(Map labels, ConfigUtils.Prefix p * 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 f98464650..93024062e 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 @@ -70,7 +70,7 @@ public final SourceData compute(String sourceName, ConfigUtils.Prefix prefix, St return new SourceData(generateSourceName(target, names, namespace, activeProfiles), data.data()); } - 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); } @@ -81,6 +81,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/SecretsCache.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsCache.java index 90a35bc26..3c8500c06 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 c8ddda3de..b1d09bd9f 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 2eb33c801..a86c4626f 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 @@ -86,11 +86,11 @@ public PropertySource locate(Environment environment) { putPathConfig(composite); if (this.properties.enableApi()) { - uniqueSources - .forEach(s -> composite.addPropertySource(getSecretsPropertySourceForSingleSecret(env, s))); + uniqueSources.forEach(s -> composite.addPropertySource( + getSecretsPropertySourceForSingleSecret(env, s, properties.namespacedBatchRead()))); } - cache.discardAll(); + cache.discardSecrets(); return composite; } return null; @@ -102,13 +102,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/main/java/org/springframework/cloud/kubernetes/commons/config/SourceData.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceData.java index 29b4280e2..83d27f0bb 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceData.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SourceData.java @@ -25,7 +25,7 @@ * * @author wind57 */ -public final record SourceData(String sourceName, Map sourceData) { +public record SourceData(String sourceName, Map sourceData) { public static SourceData emptyRecord(String sourceName) { return new SourceData(sourceName, Collections.emptyMap()); diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigPropertiesBindingTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigPropertiesBindingTests.java index bab1e9d20..4aa0f7f2b 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 @@ -53,6 +53,7 @@ void testWithDefaults() { Assertions.assertEquals(props.retry().maxInterval(), 2000L); Assertions.assertEquals(props.retry().maxAttempts(), 6); Assertions.assertTrue(props.retry().enabled()); + Assertions.assertTrue(props.namespacedBatchRead()); }); } @@ -77,7 +78,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.assertNotNull(props); @@ -113,7 +115,7 @@ void testWithNonDefaults() { Assertions.assertEquals(retryProperties.multiplier(), 1.2); Assertions.assertEquals(retryProperties.maxInterval(), 3); Assertions.assertFalse(retryProperties.enabled()); - + Assertions.assertFalse(props.namespacedBatchRead()); }); } 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 f4b0a98cc..40354b053 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.assertEquals(sources.size(), 1, "empty sources must generate a List with a single NormalizedSource"); @@ -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.assertEquals(sources.size(), 1, "empty sources must generate a List with a single NormalizedSource"); @@ -105,7 +105,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.assertEquals(sources.size(), 1, "a single NormalizedSource is expected"); @@ -148,7 +148,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.assertEquals(sources.size(), 3, "3 NormalizedSources are expected"); @@ -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.assertEquals(sources.size(), 4, "4 NormalizedSources are expected"); @@ -230,7 +230,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.assertEquals(sources.size(), 1, "empty sources must generate a List with a single NormalizedSource"); @@ -259,7 +259,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.assertEquals(sources.size(), 1, "empty sources must generate a List with a single NormalizedSource"); @@ -303,7 +303,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.assertEquals(sources.size(), 3); @@ -361,7 +361,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/KubernetesConfigDataLocationResolverTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigDataLocationResolverTests.java index e830ac4a0..59369be80 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 @@ -225,10 +225,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 9f0774f35..a30120d2e 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.assertEquals(left.hashCode(), right.hashCode()); Assertions.assertEquals(left, 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.assertEquals(left.hashCode(), right.hashCode()); Assertions.assertEquals(left, 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.assertSame(source.type(), NormalizedSourceType.LABELED_SECRET); } @Test void testImmutableGetLabels() { - LabeledSecretNormalizedSource source = new LabeledSecretNormalizedSource("namespace", labels, false, false); + LabeledSecretNormalizedSource source = new LabeledSecretNormalizedSource("namespace", labels, false); Assertions.assertThrows(RuntimeException.class, () -> source.labels().put("c", "d")); } @Test void testTarget() { - LabeledSecretNormalizedSource source = new LabeledSecretNormalizedSource("namespace", labels, false, false); + LabeledSecretNormalizedSource source = new LabeledSecretNormalizedSource("namespace", labels, false); Assertions.assertEquals(source.target(), "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.assertTrue(source.name().isEmpty()); Assertions.assertEquals(source.namespace().get(), "namespace"); Assertions.assertFalse(source.failFast()); - Assertions.assertTrue(source.profileSpecificSources()); } @Test void testConstructorWithoutPrefixFields() { - LabeledSecretNormalizedSource source = new LabeledSecretNormalizedSource("namespace", labels, true, true); + LabeledSecretNormalizedSource source = new LabeledSecretNormalizedSource("namespace", labels, true); Assertions.assertEquals(source.namespace().get(), "namespace"); Assertions.assertTrue(source.failFast()); Assertions.assertSame(ConfigUtils.Prefix.DEFAULT, source.prefix()); - Assertions.assertTrue(source.profileSpecificSources()); } } 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 0e390eabd..b713ac6cb 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 @@ -53,6 +53,7 @@ void testWithDefaults() { Assertions.assertEquals(props.retry().maxInterval(), 2000L); Assertions.assertEquals(props.retry().maxAttempts(), 6); Assertions.assertTrue(props.retry().enabled()); + Assertions.assertTrue(props.namespacedBatchRead()); }); } @@ -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.assertNotNull(props); @@ -113,7 +115,7 @@ void testWithNonDefaults() { Assertions.assertEquals(retryProperties.multiplier(), 1.2); Assertions.assertEquals(retryProperties.maxInterval(), 3); Assertions.assertFalse(retryProperties.enabled()); - + Assertions.assertFalse(props.namespacedBatchRead()); }); } 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 cb3cd6439..12a29fc46 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 @@ -40,7 +40,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.assertEquals(source.size(), 1); @@ -81,7 +81,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.assertEquals(result.size(), 6); @@ -126,7 +126,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.assertEquals(sources.size(), 1, "empty sources must generate a List with a single NormalizedSource"); @@ -152,7 +152,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.assertEquals(sources.size(), 1, "empty sources must generate a List with a single NormalizedSource"); @@ -184,7 +184,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.assertEquals(sources.size(), 1, "a single NormalizedSource is expected"); @@ -227,7 +227,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.assertEquals(sources.size(), 3, "3 NormalizedSources are expected"); @@ -277,7 +277,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.assertEquals(sources.size(), 4, "4 NormalizedSources are expected"); @@ -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.assertEquals(labeled1.prefix().prefixProvider().get(), "one"); - Assertions.assertFalse(labeled1.profileSpecificSources()); LabeledSecretNormalizedSource labeled3 = (LabeledSecretNormalizedSource) sources.get(3); Assertions.assertEquals(labeled3.prefix().prefixProvider().get(), "two"); - Assertions.assertTrue(labeled3.profileSpecificSources()); LabeledSecretNormalizedSource labeled5 = (LabeledSecretNormalizedSource) sources.get(5); Assertions.assertEquals(labeled5.prefix().prefixProvider().get(), "three"); - Assertions.assertFalse(labeled5.profileSpecificSources()); LabeledSecretNormalizedSource labeled7 = (LabeledSecretNormalizedSource) sources.get(7); Assertions.assertSame(labeled7.prefix(), ConfigUtils.Prefix.DEFAULT); - Assertions.assertFalse(labeled7.profileSpecificSources()); Set set = new LinkedHashSet<>(sources); Assertions.assertEquals(5, set.size()); 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 1028151d1..d24d19ea5 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 @@ -80,7 +80,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)); }); @@ -100,7 +100,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 f34aed50a..740b7ff8c 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,13 +30,14 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; 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.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; @@ -128,13 +129,13 @@ public 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; @@ -144,7 +145,7 @@ public 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; @@ -152,12 +153,19 @@ public static void before() { } @AfterEach - public void after() { - new KubernetesClientConfigMapsCache().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))) @@ -191,7 +199,7 @@ public void testApplicationCase() throws ApiException { } @Test - public void testStoresCase() throws ApiException { + void testStoresCase() 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))) @@ -238,7 +246,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))) @@ -302,7 +310,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))) @@ -343,7 +351,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))) @@ -357,7 +365,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 3b2e7a5e0..ba1991bf9 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 @@ -28,10 +28,15 @@ import io.kubernetes.client.openapi.models.V1SecretBuilder; import io.kubernetes.client.openapi.models.V1SecretList; import io.kubernetes.client.openapi.models.V1SecretListBuilder; +import org.junit.jupiter.api.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.KubernetesClientSourcesNamespaceBatched; import org.springframework.cloud.kubernetes.commons.config.Constants; import static org.assertj.core.api.Assertions.assertThat; @@ -71,7 +76,7 @@ class KubernetesPropertySourceSupplierTests { .build(); @BeforeAll - public static void before() throws ApiException { + static void beforeAll() throws ApiException { 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))) .thenReturn(CONFIGMAP_DEFAULT_LIST); @@ -93,6 +98,23 @@ public static void before() throws ApiException { .thenReturn(SECRET_TEAM_B_LIST); } + @AfterAll + static void afterAll() { + Mockito.reset(coreApi); + } + + @AfterEach + void afterEach() { + new KubernetesClientSourcesNamespaceBatched().discardConfigMaps(); + new KubernetesClientSourcesNamespaceBatched().discardSecrets(); + } + + @BeforeEach + void beforeEach() { + new KubernetesClientSourcesNamespaceBatched().discardConfigMaps(); + new KubernetesClientSourcesNamespaceBatched().discardSecrets(); + } + @Test void whenCurrentAndExtraNamespacesAddedThenAllConfigMapsAreIncluded() { KubernetesConfigServerProperties kubernetesConfigServerProperties = new KubernetesConfigServerProperties(); 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 0e51ac0ce..517a0d056 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 08c9c71f5..a82460535 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 @@ -63,7 +63,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, @@ -75,7 +75,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 2231293e7..bf9a88314 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 9c533d473..c77271ac1 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 9dd494fc2..000000000 --- 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 2d098ebfa..f7a1a765e 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 f7c18ce14..c5abbafc9 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 000000000..ad258a2da --- /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 000000000..ceada7354 --- /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 8058f041d..4c321fa42 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 efad27a7b..4b7e4672c 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 3ac61a7ca..896d4122e 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 5a2356085..764d3a866 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 b478432e9..f72dfcdbf 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 8c08f8449..ec093076e 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/Fabric8ConfigMapPropertySourceLocatorMockTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocatorMockTests.java index 2e9f1421a..8dbbcf3fe 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 e289bbc43..ffd8ff14a 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 @@ -51,7 +51,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())); @@ -69,7 +69,7 @@ void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() { 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 912ac2d30..5a489a644 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 482e8ba32..986a364b1 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 @@ -44,7 +44,7 @@ class Fabric8ConfigMapPropertySourceTests { @AfterEach void afterEach() { - new Fabric8ConfigMapsCache().discardAll(); + new Fabric8SourcesNamespaceBatched().discardConfigMaps(); } @Test @@ -55,7 +55,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."); } @@ -68,7 +69,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, "", 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 000000000..3e56fd334 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigUtilsNamespacedBatchReadTests.java @@ -0,0 +1,383 @@ +/* + * 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 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.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +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.assertEquals(Map.of(), result.data()); + Assertions.assertTrue(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.assertEquals(Set.of("my-secret"), result.names()); + Assertions.assertEquals(Map.of("property", "value"), result.data()); + } + + /** + *
+	 * 		- 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.assertEquals(Set.of("my-secret"), result.names()); + Assertions.assertEquals(Map.of("key1", "value1"), result.data()); + } + + /** + *
+	 * 		- 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.assertTrue(result.names().contains("my-secret")); + Assertions.assertTrue(result.names().contains("my-secret-2")); + + Assertions.assertEquals(2, result.data().size()); + Assertions.assertEquals("value", result.data().get("property")); + Assertions.assertEquals("value-2", result.data().get("property-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.assertTrue(result.names().contains("blue-circle-secret")); + Assertions.assertTrue(result.names().contains("blue-square-secret")); + + Assertions.assertEquals(2, result.data().size()); + Assertions.assertEquals("1", result.data().get("one")); + Assertions.assertEquals("2", result.data().get("two")); + } + + /** + *
+	 * 		- 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.assertEquals(0, result.names().size()); + Assertions.assertEquals(0, result.data().size()); + } + + /** + *
+	 * 		- 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.assertEquals(1, result.names().size()); + Assertions.assertEquals("value", result.data().get("property")); + } + + /** + *
+	 * 		- 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.assertEquals(Set.of("my-config-map"), result.names()); + Assertions.assertTrue(result.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.assertEquals(Set.of(), result.names()); + Assertions.assertTrue(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.assertEquals(Set.of("my-config-map"), result.names()); + Assertions.assertEquals(Map.of("property", "value"), result.data()); + } + + /** + *
+	 *     - 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.assertEquals(Set.of("my-config-map"), result.names()); + Assertions.assertEquals(Map.of("key1", "value1"), result.data()); + } + + /** + *
+	 *     - 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.assertTrue(result.names().contains("my-config-map")); + Assertions.assertTrue(result.names().contains("my-config-map-2")); + + Assertions.assertEquals(2, result.data().size()); + Assertions.assertEquals("value", result.data().get("property")); + Assertions.assertEquals("value-2", result.data().get("property-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 000000000..f589db42d --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigUtilsNonNamespacedBatchReadTests.java @@ -0,0 +1,383 @@ +/* + * 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 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.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +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.assertEquals(Map.of(), result.data()); + Assertions.assertTrue(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.assertEquals(Set.of("my-secret"), result.names()); + Assertions.assertEquals(Map.of("property", "value"), result.data()); + } + + /** + *
+	 * 		- 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.assertEquals(Set.of("my-secret"), result.names()); + Assertions.assertEquals(Map.of("key1", "value1"), result.data()); + } + + /** + *
+	 * 		- 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.assertTrue(result.names().contains("my-secret")); + Assertions.assertTrue(result.names().contains("my-secret-2")); + + Assertions.assertEquals(2, result.data().size()); + Assertions.assertEquals("value", result.data().get("property")); + Assertions.assertEquals("value-2", result.data().get("property-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.assertTrue(result.names().contains("blue-circle-secret")); + Assertions.assertTrue(result.names().contains("blue-square-secret")); + + Assertions.assertEquals(2, result.data().size()); + Assertions.assertEquals("1", result.data().get("one")); + Assertions.assertEquals("2", result.data().get("two")); + } + + /** + *
+	 * 		- 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.assertEquals(0, result.names().size()); + Assertions.assertEquals(0, result.data().size()); + } + + /** + *
+	 * 		- 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.assertEquals(1, result.names().size()); + Assertions.assertEquals("value", result.data().get("property")); + } + + /** + *
+	 * 		- 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.assertEquals(Set.of("my-config-map"), result.names()); + Assertions.assertTrue(result.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.assertEquals(Set.of(), result.names()); + Assertions.assertTrue(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.assertEquals(Set.of("my-config-map"), result.names()); + Assertions.assertEquals(Map.of("property", "value"), result.data()); + } + + /** + *
+	 *     - 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.assertEquals(Set.of("my-config-map"), result.names()); + Assertions.assertEquals(Map.of("key1", "value1"), result.data()); + } + + /** + *
+	 *     - 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.assertTrue(result.names().contains("my-config-map")); + Assertions.assertTrue(result.names().contains("my-config-map-2")); + + Assertions.assertEquals(2, result.data().size()); + Assertions.assertEquals("value", result.data().get("property")); + Assertions.assertEquals("value-2", result.data().get("property-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 843a2e82a..0113ed392 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,360 +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.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; 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.assertEquals(Map.of(), result.data()); - Assertions.assertTrue(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.assertEquals(Set.of("my-secret"), result.names()); - Assertions.assertEquals(Map.of("property", "value"), result.data()); - } - - // 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.assertEquals(Set.of("my-secret"), result.names()); - Assertions.assertEquals(Map.of("key1", "value1"), result.data()); - } - - // 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.assertTrue(result.names().contains("my-secret")); - Assertions.assertTrue(result.names().contains("my-secret-2")); - - Assertions.assertEquals(2, result.data().size()); - Assertions.assertEquals("value", result.data().get("property")); - Assertions.assertEquals("value-2", result.data().get("property-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.assertTrue(result.names().contains("blue-circle-secret")); - Assertions.assertTrue(result.names().contains("blue-square-secret")); - Assertions.assertTrue(result.names().contains("blue-square-secret-k8s")); - - Assertions.assertEquals(3, result.data().size()); - Assertions.assertEquals("1", result.data().get("one")); - Assertions.assertEquals("2", result.data().get("two")); - Assertions.assertEquals("4", result.data().get("four")); - } - - // 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.assertEquals(0, result.names().size()); - Assertions.assertEquals(0, result.data().size()); - } - - // 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.assertEquals(1, result.names().size()); - Assertions.assertEquals("value", result.data().get("property")); - } - - // 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.assertTrue(result.names().contains("my-secret")); - Assertions.assertTrue(result.names().contains("my-secret-2")); - - Assertions.assertEquals(2, result.data().size()); - Assertions.assertEquals("value", result.data().get("property")); - Assertions.assertEquals("value-2", result.data().get("property-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.assertEquals(Set.of("my-config-map"), result.names()); - Assertions.assertTrue(result.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.assertEquals(Set.of(), result.names()); - Assertions.assertTrue(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.assertEquals(Set.of("my-config-map"), result.names()); - Assertions.assertEquals(Map.of("property", "value"), result.data()); - } - - // 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.assertEquals(Set.of("my-config-map"), result.names()); - Assertions.assertEquals(Map.of("key1", "value1"), result.data()); - } - - // 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.assertTrue(result.names().contains("my-config-map")); - Assertions.assertTrue(result.names().contains("my-config-map-2")); - - Assertions.assertEquals(2, result.data().size()); - Assertions.assertEquals("value", result.data().get("property")); - Assertions.assertEquals("value-2", result.data().get("property-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/Fabric8SecretsPropertySourceLocatorTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceLocatorTests.java index 522349091..e19048ca6 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 @@ -51,7 +51,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())); @@ -69,7 +69,7 @@ void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() { 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 c04866748..644b56454 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 @@ -50,7 +50,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) @@ -64,8 +64,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) @@ -79,7 +80,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)); @@ -91,8 +92,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 85% 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 7342c4c56..694b4be68 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. @@ -45,7 +45,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"; @@ -80,7 +82,7 @@ static void beforeAll() { @AfterEach void afterEach() { mockClient.configMaps().inNamespace(NAMESPACE).delete(); - new Fabric8ConfigMapsCache().discardAll(); + new Fabric8SourcesNamespaceBatched().discardConfigMaps(); } /** @@ -101,7 +103,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); @@ -145,7 +147,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); @@ -174,7 +176,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); @@ -205,7 +207,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); @@ -235,7 +237,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); @@ -276,7 +278,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); @@ -301,8 +303,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() { @@ -322,11 +323,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); @@ -339,7 +340,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() { @@ -359,11 +360,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); @@ -374,47 +375,6 @@ 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. - */ - @Test - void searchWithLabelsOneConfigMapFoundAndOneFromProfileFound() { - 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") - .withLabels(Collections.singletonMap("color", "red")) - .endMetadata() - .addToData("two", "2") - .build(); - - 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", "blue"), true, ConfigUtils.Prefix.DELAYED, true); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment); - - Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); - SourceData sourceData = data.apply(context); - - Assertions.assertEquals(sourceData.sourceData().size(), 2); - Assertions.assertEquals(sourceData.sourceData().get("color-configmap.color-configmap-k8s.one"), "1"); - Assertions.assertEquals(sourceData.sourceData().get("color-configmap.color-configmap-k8s.two"), "2"); - Assertions.assertEquals(sourceData.sourceName(), "configmap.color-configmap.color-configmap-k8s.default"); - - } - /** *
 	 *     - configmap "color-configmap" with label "{color:blue}"
@@ -425,7 +385,7 @@ void searchWithLabelsOneConfigMapFoundAndOneFromProfileFound() {
 	 * 
*/ @Test - void searchWithLabelsTwoConfigMapsFoundAndOneFromProfileFound() { + void searchWithLabelsTwoConfigMapsFound() { ConfigMap colorConfigMap = new ConfigMapBuilder().withNewMetadata() .withName("color-configmap") .withLabels(Collections.singletonMap("color", "blue")) @@ -468,27 +428,20 @@ 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); - Assertions.assertEquals(sourceData.sourceData().size(), 4); - Assertions.assertEquals(sourceData.sourceData() - .get("color-configmap.color-configmap-k8s.shape-configmap.shape-configmap-k8s.one"), "1"); - Assertions.assertEquals(sourceData.sourceData() - .get("color-configmap.color-configmap-k8s.shape-configmap.shape-configmap-k8s.two"), "2"); - Assertions.assertEquals(sourceData.sourceData() - .get("color-configmap.color-configmap-k8s.shape-configmap.shape-configmap-k8s.four"), "4"); - Assertions.assertEquals(sourceData.sourceData() - .get("color-configmap.color-configmap-k8s.shape-configmap.shape-configmap-k8s.five"), "5"); + Assertions.assertEquals(sourceData.sourceData().size(), 2); + Assertions.assertEquals(sourceData.sourceData().get("color-configmap.shape-configmap.one"), "1"); + Assertions.assertEquals(sourceData.sourceData().get("color-configmap.shape-configmap.two"), "2"); - Assertions.assertEquals(sourceData.sourceName(), - "configmap.color-configmap.color-configmap-k8s.shape-configmap.shape-configmap-k8s.default"); + Assertions.assertEquals(sourceData.sourceName(), "configmap.color-configmap.shape-configmap.default"); } @@ -524,18 +477,20 @@ 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); Assertions.assertEquals(redSourceData.sourceData().size(), 1); Assertions.assertEquals(redSourceData.sourceData().get("red-configmap.one"), "1"); + Assertions.assertTrue(output.getAll().contains("Loaded all config maps in namespace '" + NAMESPACE + "'")); + Assertions.assertFalse(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); + 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 000000000..e086efc79 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledConfigMapContextToSourceDataProviderNonNamespacedBatchReadTests.java @@ -0,0 +1,510 @@ +/* + * 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.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +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.assertEquals("configmap.test-configmap.default", sourceData.sourceName()); + Assertions.assertEquals(Map.of("name", "value"), sourceData.sourceData()); + + } + + /** + * 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.assertEquals(sourceData.sourceName(), "configmap.red-configmap.red-configmap-again.default"); + Assertions.assertEquals(sourceData.sourceData().size(), 2); + Assertions.assertEquals(sourceData.sourceData().get("colorOne"), "really-red"); + Assertions.assertEquals(sourceData.sourceData().get("colorTwo"), "really-red-again"); + + } + + /** + * one 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.assertEquals(sourceData.sourceName(), "configmap.color.default"); + Assertions.assertEquals(sourceData.sourceData(), Collections.emptyMap()); + } + + /** + * 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.assertEquals("configmap.test-configmap.default", sourceData.sourceName()); + Assertions.assertEquals(Map.of("name", "value"), sourceData.sourceData()); + } + + /** + * 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.assertEquals("configmap.blue-configmap.default", sourceData.sourceName()); + Assertions.assertEquals(Map.of("me.what-color", "blue-color"), sourceData.sourceData()); + } + + /** + * 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.assertEquals(sourceData.sourceName(), "configmap.another-blue-configmap.blue-configmap.default"); + + Map properties = sourceData.sourceData(); + Assertions.assertEquals(2, properties.size()); + Iterator keys = properties.keySet().iterator(); + String firstKey = keys.next(); + String secondKey = keys.next(); + + if (firstKey.contains("first")) { + Assertions.assertEquals(firstKey, "another-blue-configmap.blue-configmap.first"); + } + + Assertions.assertEquals(secondKey, "another-blue-configmap.blue-configmap.second"); + Assertions.assertEquals(properties.get(firstKey), "blue"); + Assertions.assertEquals(properties.get(secondKey), "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.assertTrue(sourceData.sourceData().isEmpty()); + Assertions.assertEquals(sourceData.sourceName(), "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.assertEquals(sourceData.sourceData().size(), 1); + Assertions.assertEquals(sourceData.sourceData().get("one"), "1"); + Assertions.assertEquals(sourceData.sourceName(), "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.assertEquals(sourceData.sourceData().size(), 2); + Assertions.assertEquals(sourceData.sourceData().get("color-configmap.shape-configmap.one"), "1"); + Assertions.assertEquals(sourceData.sourceData().get("color-configmap.shape-configmap.two"), "2"); + + Assertions.assertEquals(sourceData.sourceName(), "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.assertEquals(redSourceData.sourceData().size(), 1); + Assertions.assertEquals(redSourceData.sourceData().get("red-configmap.one"), "1"); + + Assertions.assertFalse(output.getAll().contains("Loaded all config maps in namespace '" + NAMESPACE + "'")); + Assertions.assertTrue(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.assertEquals(greenSourceData.sourceData().size(), 1); + Assertions.assertEquals(greenSourceData.sourceData().get("green-configmap.two"), "2"); + + // meaning there is a single entry with such a log statement + String[] out = output.getAll().split("Loaded all config maps in namespace"); + Assertions.assertEquals(out.length, 1); + + // meaning that both reads were non cached + out = output.getAll().split("Will read individual configmaps in namespace"); + Assertions.assertEquals(out.length, 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 84% 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 835e831c8..c4831ccb4 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. @@ -48,7 +48,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"; @@ -62,14 +64,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"); @@ -83,7 +83,7 @@ static void beforeAll() { @AfterEach void afterEach() { mockClient.secrets().inNamespace(NAMESPACE).delete(); - new Fabric8SecretsCache().discardAll(); + new Fabric8SourcesNamespaceBatched().discardSecrets(); } /** @@ -102,9 +102,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); @@ -146,9 +146,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); @@ -175,9 +175,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); @@ -205,9 +205,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); @@ -235,9 +235,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); @@ -275,9 +275,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); @@ -302,8 +302,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() { @@ -323,11 +322,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); @@ -340,7 +339,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() { @@ -360,11 +359,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); @@ -375,47 +374,6 @@ 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. - */ - @Test - void searchWithLabelsOneSecretFoundAndOneFromProfileFound() { - 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") - .withLabels(Collections.singletonMap("color", "red")) - .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(); - 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); - - Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); - SourceData sourceData = data.apply(context); - - Assertions.assertEquals(sourceData.sourceData().size(), 2); - Assertions.assertEquals(sourceData.sourceData().get("color-secret.color-secret-k8s.one"), "1"); - Assertions.assertEquals(sourceData.sourceData().get("color-secret.color-secret-k8s.two"), "2"); - Assertions.assertEquals(sourceData.sourceName(), "secret.color-secret.color-secret-k8s.default"); - - } - /** *
 	 *     - secret "color-secret" with label "{color:blue}"
@@ -426,7 +384,7 @@ void searchWithLabelsOneSecretFoundAndOneFromProfileFound() {
 	 * 
*/ @Test - void searchWithLabelsTwoSecretsFoundAndOneFromProfileFound() { + void searchWithLabelsTwoSecretsFound() { Secret colorSecret = new SecretBuilder().withNewMetadata() .withName("color-secret") .withLabels(Collections.singletonMap("color", "blue")) @@ -469,27 +427,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.assertEquals(sourceData.sourceData().size(), 4); - Assertions.assertEquals( - sourceData.sourceData().get("color-secret.color-secret-k8s.shape-secret.shape-secret-k8s.one"), "1"); - Assertions.assertEquals( - sourceData.sourceData().get("color-secret.color-secret-k8s.shape-secret.shape-secret-k8s.two"), "2"); - Assertions.assertEquals( - sourceData.sourceData().get("color-secret.color-secret-k8s.shape-secret.shape-secret-k8s.four"), "4"); - Assertions.assertEquals( - sourceData.sourceData().get("color-secret.color-secret-k8s.shape-secret.shape-secret-k8s.five"), "5"); + Assertions.assertEquals(sourceData.sourceData().size(), 2); + Assertions.assertEquals(sourceData.sourceData().get("color-secret.shape-secret.one"), "1"); + Assertions.assertEquals(sourceData.sourceData().get("color-secret.shape-secret.two"), "2"); - Assertions.assertEquals(sourceData.sourceName(), - "secret.color-secret.color-secret-k8s.shape-secret.shape-secret-k8s.default"); + Assertions.assertEquals(sourceData.sourceName(), "secret.color-secret.shape-secret.default"); } @@ -508,9 +459,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); @@ -550,20 +501,22 @@ 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); Assertions.assertEquals(redSourceData.sourceData().size(), 1); Assertions.assertEquals(redSourceData.sourceData().get("red.one"), "1"); + Assertions.assertTrue(output.getAll().contains("Loaded all secrets in namespace '" + NAMESPACE + "'")); + Assertions.assertFalse(output.getAll().contains("Will read individual secrets in 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 000000000..0258c6c3f --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledSecretContextToSourceDataProviderNonNamespacedBatchReadTests.java @@ -0,0 +1,534 @@ +/* + * 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.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +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.assertEquals("secret.test-secret.default", sourceData.sourceName()); + Assertions.assertEquals(Map.of("secretName", "secretValue"), sourceData.sourceData()); + + } + + /** + * 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.assertEquals(sourceData.sourceName(), "secret.red-secret.red-secret-again.default"); + Assertions.assertEquals(sourceData.sourceData().size(), 2); + Assertions.assertEquals(sourceData.sourceData().get("colorOne"), "really-red"); + Assertions.assertEquals(sourceData.sourceData().get("colorTwo"), "really-red-again"); + + } + + /** + * one secret deployed (pink), does not match our query (blue). + */ + @Test + void secretNoMatch() { + + Secret pink = new SecretBuilder().withNewMetadata() + .withName("pink-secret") + .withLabels(PINK_LABEL) + .endMetadata() + .addToData("color", Base64.getEncoder().encodeToString("pink".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).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.assertEquals(sourceData.sourceName(), "secret.color.default"); + Assertions.assertEquals(sourceData.sourceData(), Collections.emptyMap()); + } + + /** + * LabeledSecretContextToSourceDataProvider gets as input a Fabric8ConfigContext. This + * context has a namespace as well as a NormalizedSource, that has a namespace too. It + * is easy to get confused in code on which namespace to use. This test makes sure + * that we use the proper one. + */ + @Test + void namespaceMatch() { + + Secret secret = new SecretBuilder().withNewMetadata() + .withName("test-secret") + .withLabels(LABELS) + .endMetadata() + .addToData("secretName", Base64.getEncoder().encodeToString("secretValue".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).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.assertEquals("secret.test-secret.default", sourceData.sourceName()); + Assertions.assertEquals(Map.of("secretName", "secretValue"), sourceData.sourceData()); + } + + /** + * 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.assertEquals("secret.blue-secret.default", sourceData.sourceName()); + Assertions.assertEquals(Map.of("me.what-color", "blue-color"), sourceData.sourceData()); + } + + /** + * 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.assertEquals(sourceData.sourceName(), "secret.another-blue-secret.blue-secret.default"); + + Map properties = sourceData.sourceData(); + Assertions.assertEquals(2, properties.size()); + Iterator keys = properties.keySet().iterator(); + String firstKey = keys.next(); + String secondKey = keys.next(); + + if (firstKey.contains("first")) { + Assertions.assertEquals(firstKey, "another-blue-secret.blue-secret.first"); + } + + Assertions.assertEquals(secondKey, "another-blue-secret.blue-secret.second"); + Assertions.assertEquals(properties.get(firstKey), "blue"); + Assertions.assertEquals(properties.get(secondKey), "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.assertTrue(sourceData.sourceData().isEmpty()); + Assertions.assertEquals(sourceData.sourceName(), "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.assertEquals(sourceData.sourceData().size(), 1); + Assertions.assertEquals(sourceData.sourceData().get("one"), "1"); + Assertions.assertEquals(sourceData.sourceName(), "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.assertEquals(sourceData.sourceData().size(), 2); + Assertions.assertEquals(sourceData.sourceData().get("color-secret.shape-secret.one"), "1"); + Assertions.assertEquals(sourceData.sourceData().get("color-secret.shape-secret.two"), "2"); + + Assertions.assertEquals(sourceData.sourceName(), "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.assertEquals(sourceData.sourceData().size(), 1); + Assertions.assertEquals(sourceData.sourceData().get("color"), "blue"); + Assertions.assertEquals(sourceData.sourceName(), "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.assertEquals(redSourceData.sourceData().size(), 1); + Assertions.assertEquals(redSourceData.sourceData().get("red.one"), "1"); + + Assertions.assertFalse(output.getAll().contains("Loaded all secrets in namespace '" + NAMESPACE + "'")); + Assertions.assertTrue(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.assertEquals(greenSourceData.sourceData().size(), 1); + Assertions.assertEquals(greenSourceData.sourceData().get("green.two"), "2"); + + // meaning there is a single entry with such a log statement + String[] out = output.getAll().split("Loaded all secrets in namespace"); + Assertions.assertEquals(out.length, 1); + + // meaning that the second read was done from the cache + out = output.getAll().split("Will read individual secrets in namespace"); + Assertions.assertEquals(out.length, 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 79f2f134e..40522cf6c 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 @@ -45,7 +45,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"; @@ -71,7 +73,7 @@ static void beforeAll() { @AfterEach void afterEach() { mockClient.configMaps().inNamespace(NAMESPACE).delete(); - new Fabric8ConfigMapsCache().discardAll(); + new Fabric8SourcesNamespaceBatched().discardConfigMaps(); } /** @@ -93,7 +95,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); @@ -122,7 +124,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); @@ -163,7 +165,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); @@ -208,7 +211,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); @@ -259,7 +263,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); @@ -290,7 +295,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); @@ -320,7 +325,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); @@ -346,7 +351,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); @@ -376,7 +381,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); @@ -413,18 +419,22 @@ 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); Assertions.assertEquals(redSourceData.sourceName(), "configmap.red.default"); Assertions.assertEquals(redSourceData.sourceData().size(), 1); Assertions.assertEquals(redSourceData.sourceData().get("some.color"), "really-red"); + Assertions.assertTrue(output.getAll().contains("Loaded all config maps in namespace '" + NAMESPACE + "'")); + Assertions.assertFalse(output.getOut().contains("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 000000000..1a45d515f --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/NamedConfigMapContextToSourceDataProviderNonNamespacedBatchReadTests.java @@ -0,0 +1,454 @@ +/* + * 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.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +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.assertEquals(sourceData.sourceName(), "configmap.blue.default"); + Assertions.assertEquals(sourceData.sourceData(), Collections.emptyMap()); + + } + + /** + *
+	 *     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.assertEquals(sourceData.sourceName(), "configmap.red.default"); + Assertions.assertEquals(sourceData.sourceData(), 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.assertEquals(sourceData.sourceName(), "configmap.red.red-with-profile.default.with-profile"); + Assertions.assertEquals(sourceData.sourceData().size(), 2); + Assertions.assertEquals(sourceData.sourceData().get("color"), "really-red"); + Assertions.assertEquals(sourceData.sourceData().get("taste"), "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.assertEquals(sourceData.sourceName(), "configmap.red.red-with-profile.default"); + Assertions.assertEquals(sourceData.sourceData().size(), 2); + Assertions.assertEquals(sourceData.sourceData().get("some.color"), "really-red"); + Assertions.assertEquals(sourceData.sourceData().get("some.taste"), "mango"); + + } + + /** + *
+	 *     - 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.assertEquals(sourceData.sourceName(), "configmap.red.red-with-shape.red-with-taste.default"); + Assertions.assertEquals(sourceData.sourceData().size(), 3); + Assertions.assertEquals(sourceData.sourceData().get("some.color"), "really-red"); + Assertions.assertEquals(sourceData.sourceData().get("some.taste"), "mango"); + Assertions.assertEquals(sourceData.sourceData().get("some.shape"), "round"); + + } + + /** + *
+	 * 		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.assertEquals(sourceData.sourceName(), "configmap.application.default"); + Assertions.assertEquals(sourceData.sourceData(), 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.assertEquals(sourceData.sourceName(), "configmap.red.default"); + Assertions.assertEquals(sourceData.sourceData(), 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.assertEquals(sourceData.sourceName(), "configmap.red.default"); + Assertions.assertEquals(sourceData.sourceData(), 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.assertEquals(sourceData.sourceName(), "configmap.one.default"); + Assertions.assertEquals(sourceData.sourceData(), 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.assertEquals(redSourceData.sourceName(), "configmap.red.default"); + Assertions.assertEquals(redSourceData.sourceData().size(), 1); + Assertions.assertEquals(redSourceData.sourceData().get("some.color"), "really-red"); + + Assertions.assertFalse(output.getAll().contains("Loaded all config maps in namespace '" + NAMESPACE + "'")); + Assertions.assertTrue( + 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.assertEquals(greenSourceData.sourceName(), "configmap.green.default"); + Assertions.assertEquals(greenSourceData.sourceData().size(), 1); + Assertions.assertEquals(greenSourceData.sourceData().get("some.taste"), "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.assertEquals(out.length, 1); + + // meaning that the second read was not from the cache + out = output.getAll().split("Will read individual configmaps in namespace : default"); + Assertions.assertEquals(out.length, 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 2888d3329..2dfd2d0fa 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. @@ -46,7 +46,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"; @@ -70,7 +72,7 @@ static void beforeAll() { @AfterEach void afterEach() { mockClient.secrets().inNamespace(NAMESPACE).delete(); - new Fabric8SecretsCache().discardAll(); + new Fabric8SourcesNamespaceBatched().discardSecrets(); } /** @@ -89,7 +91,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); @@ -222,7 +224,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); @@ -264,7 +267,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); @@ -313,7 +317,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); @@ -345,7 +350,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); @@ -381,18 +386,22 @@ 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); Assertions.assertEquals(redSourceData.sourceName(), "secret.red.default"); Assertions.assertEquals(redSourceData.sourceData().size(), 1); Assertions.assertEquals(redSourceData.sourceData().get("some.color"), "red"); + Assertions.assertTrue(output.getAll().contains("Loaded all secrets in namespace '" + NAMESPACE + "'")); + Assertions.assertFalse(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); + 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 000000000..10d7ae672 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/NamedSecretContextToSourceDataProviderNonNamespacedBatchReadTests.java @@ -0,0 +1,420 @@ +/* + * 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.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +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.assertEquals(sourceData.sourceName(), "secret.red.default"); + Assertions.assertEquals(sourceData.sourceData(), Map.of("color", "really-red")); + + } + + /** + * we have three secret deployed. one of them has a name that matches (red), the other + * two have different names, thus no match. + */ + @Test + void twoSecretMatchAgainstLabels() { + + Secret red = new SecretBuilder().withNewMetadata() + .withName("red") + .endMetadata() + .addToData("color", Base64.getEncoder().encodeToString("really-red".getBytes())) + .build(); + + Secret blue = new SecretBuilder().withNewMetadata() + .withName("blue") + .endMetadata() + .addToData("color", Base64.getEncoder().encodeToString("really-blue".getBytes())) + .build(); + + Secret yellow = new SecretBuilder().withNewMetadata() + .withName("yellow") + .endMetadata() + .addToData("color", Base64.getEncoder().encodeToString("really-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.assertEquals(sourceData.sourceName(), "secret.red.default"); + Assertions.assertEquals(sourceData.sourceData().size(), 1); + Assertions.assertEquals(sourceData.sourceData().get("color"), "really-red"); + + } + + /** + * one secret deployed (pink), does not match our query (blue). + */ + @Test + void testSecretNoMatch() { + + Secret pink = new SecretBuilder().withNewMetadata() + .withName("pink") + .endMetadata() + .addToData("color", Base64.getEncoder().encodeToString("pink".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).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.assertEquals(sourceData.sourceName(), "secret.blue.default"); + Assertions.assertEquals(sourceData.sourceData(), Collections.emptyMap()); + } + + /** + * NamedSecretContextToSourceDataProvider gets as input a Fabric8ConfigContext. This + * context has a namespace as well as a NormalizedSource, that has a namespace too. It + * is easy to get confused in code on which namespace to use. This test makes sure + * that we use the proper one. + */ + @Test + void namespaceMatch() { + + Secret secret = new SecretBuilder().withNewMetadata() + .withName("red") + .endMetadata() + .addToData("color", Base64.getEncoder().encodeToString("really-red".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).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.assertEquals(sourceData.sourceName(), "secret.red.default"); + Assertions.assertEquals(sourceData.sourceData(), 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.assertEquals(sourceData.sourceName(), "secret.red.red-with-profile.default.with-profile"); + Assertions.assertEquals(sourceData.sourceData().size(), 2); + Assertions.assertEquals(sourceData.sourceData().get("color"), "really-red"); + Assertions.assertEquals(sourceData.sourceData().get("taste"), "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.assertEquals(sourceData.sourceName(), "secret.red.red-with-profile.default"); + Assertions.assertEquals(sourceData.sourceData().size(), 2); + Assertions.assertEquals(sourceData.sourceData().get("some.color"), "really-red"); + Assertions.assertEquals(sourceData.sourceData().get("some.taste"), "mango"); + + } + + /** + * we have three 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.assertEquals(sourceData.sourceName(), "secret.red.red-with-shape.red-with-taste.default"); + + Assertions.assertEquals(sourceData.sourceData().size(), 3); + Assertions.assertEquals(sourceData.sourceData().get("some.color"), "really-red"); + Assertions.assertEquals(sourceData.sourceData().get("some.taste"), "mango"); + Assertions.assertEquals(sourceData.sourceData().get("some.shape"), "round"); + + } + + /** + *
+	 *     - 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.assertEquals(sourceData.sourceName(), "secret.single-yaml.default"); + Assertions.assertEquals(sourceData.sourceData(), 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.assertEquals(redSourceData.sourceName(), "secret.red.default"); + Assertions.assertEquals(redSourceData.sourceData().size(), 1); + Assertions.assertEquals(redSourceData.sourceData().get("some.color"), "red"); + + Assertions.assertFalse(output.getAll().contains("Loaded all secrets in namespace '" + NAMESPACE + "'")); + Assertions.assertTrue(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.assertEquals(greenSourceData.sourceName(), "secret.green.default"); + Assertions.assertEquals(greenSourceData.sourceData().size(), 1); + Assertions.assertEquals(greenSourceData.sourceData().get("some.taste"), "mango"); + + // meaning there is a single entry with such a log statement + String[] out = output.getAll().split("Loaded all secrets in namespace"); + Assertions.assertEquals(out.length, 1); + + // meaning that the second read was done from the cache + out = output.getAll().split("Will read individual secrets in namespace"); + Assertions.assertEquals(out.length, 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 298f337e8..0a28020a2 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 @@ -53,9 +53,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}" * @@ -74,7 +71,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")); @@ -82,13 +79,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"); @@ -104,7 +101,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")); } @@ -123,7 +120,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" * */ @@ -141,9 +138,8 @@ void testBlue() { /** *
 	 *   this one is taken from : "green-configmap.green-configmap-k8s.green-configmap-prod.green-purple-configmap.green-purple-configmap-k8s".
-	 *   We find "green-configmap" by labels, also "green-configmap-k8s" and "green-configmap-prod" exists,
-	 *   because "includeProfileSpecificSources=true" is set. Also "green-purple-configmap" and "green-purple-configmap-k8s"
-	 * 	 are found.
+	 *   We find "green-configmap", "green-configmap-k8s", "green-configmap-prod"  by labels.
+	 *   Also "green-purple-configmap" and "green-purple-configmap-k8s" are found.
 	 * 
*/ @Test 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 184a8242b..73212476b 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 @@ -54,9 +54,6 @@ 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}" - * - * # 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}" * @@ -76,7 +73,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")); @@ -86,15 +83,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", @@ -114,7 +111,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")); } @@ -133,7 +130,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" * */ @@ -151,9 +148,8 @@ void testBlue() { /** *
 	 *   this one is taken from : "green-purple-secret.green-purple-secret-k8s.green-secret.green-secret-k8s.green-secret-prod".
-	 *   We find "green-secret" by labels, also "green-secrets-k8s" and "green-secrets-prod" exists,
-	 *   because "includeProfileSpecificSources=true" is set. Also "green-purple-secret" and "green-purple-secret-k8s"
-	 * 	 are found.
+	 *   We find "green-secret", "green-secrets-k8s" and "green-secrets-prod" by labels.
+	 *   Also "green-purple-secret" and "green-purple-secret-k8s" are found.
 	 * 
*/ @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 985e0cc9f..3059aa85b 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/resources/labeled-configmap-with-profile.yaml b/spring-cloud-kubernetes-fabric8-config/src/test/resources/labeled-configmap-with-profile.yaml index 8aa88d2de..5cde6f074 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 89255f081..b22cac4bf 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: