diff --git a/docs/modules/ROOT/pages/property-source-config/configmap-propertysource.adoc b/docs/modules/ROOT/pages/property-source-config/configmap-propertysource.adoc index 2a3fc505ab..9c7ac26298 100644 --- a/docs/modules/ROOT/pages/property-source-config/configmap-propertysource.adoc +++ b/docs/modules/ROOT/pages/property-source-config/configmap-propertysource.adoc @@ -540,7 +540,7 @@ spring: includeProfileSpecificSources: false ---- -NOTE: Since version `5.0.0-M1`, `includeProfileSpecificSources` is only supported for named sources (`spring.cloud.kubernetes.sources.name=XXX`); support for labeled sources has been removed. +NOTE: Since version `5.0.0`, `includeProfileSpecificSources` is only supported for named sources (`spring.cloud.kubernetes.sources.name=XXX`); support for labeled sources has been removed. Notice that just like before, there are two levels where you can specify this property: for all config maps or @@ -588,6 +588,9 @@ NOTE: If you already have `spring-retry` and `spring-boot-starter-aspectj` on th and want to enable fail-fast, but do not want retry to be enabled; you can disable retry for `ConfigMap` `PropertySources` by setting `spring.cloud.kubernetes.config.retry.enabled=false`. + +NOTE: Since version `5.0.0`, we have introduced the possibility to read sources individually. Until now, we would go to the namespace and read all the configmaps / secrets available and then filter out the ones requested. Since `5.0.0-M3` you can specify that you want to read them individually, by setting the property: `spring.cloud.kubernetes.config.read-type=SINGLE`. The previous option to read them all in a namespace is controlled by `spring.cloud.kubernetes.config.read-type=BATCH` and it is the default option. + .Properties: [options="header,footer"] |=== diff --git a/docs/modules/ROOT/pages/spring-cloud-kubernetes-configserver.adoc b/docs/modules/ROOT/pages/spring-cloud-kubernetes-configserver.adoc index 2ac8c3b078..6871ce5f71 100644 --- a/docs/modules/ROOT/pages/spring-cloud-kubernetes-configserver.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-kubernetes-configserver.adoc @@ -11,17 +11,17 @@ A default image is located on https://hub.docker.com/r/springcloud/spring-cloud- the code and image yourself. However, if you need to customize the config server behavior or prefer to build the image yourself you can easily build your own image from the https://github.com/spring-cloud/spring-cloud-kubernetes/tree/main/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver[source code on GitHub] and use that. -## Configuration +== Configuration -### Enabling The Kubernetes Environment Repository +=== Enabling The Kubernetes Environment Repository To enable the Kubernetes environment repository the `kubernetes` profile must be included in the list of active profiles. You may activate other profiles as well to use other environment repository implementations. -### Config Map and Secret PropertySources +=== Config Map and Secret PropertySources By default, only Config Map data will be fetched. To enable Secrets as well you will need to set `spring.cloud.kubernetes.secrets.enableApi=true`. You can disable the Config Map `PropertySource` by setting `spring.cloud.kubernetes.config.enableApi=false`. -### Fetching Config Map and Secret Data From Additional Namespaces +=== Fetching Config Map and Secret Data From Additional Namespaces By default, the Kubernetes environment repository will only fetch Config Map and Secrets from the namespace in which it is deployed. If you want to include data from other namespaces you can set `spring.cloud.kubernetes.configserver.config-map-namespaces` and/or `spring.cloud.kubernetes.configserver.secrets-namespaces` to a comma separated list of namespace values. @@ -29,19 +29,21 @@ list of namespace values. NOTE: If you set `spring.cloud.kubernetes.configserver.config-map-namespaces` and/or `spring.cloud.kubernetes.configserver.secrets-namespaces` you will need to include the namespace in which the Config Server is deployed in order to continue to fetch Config Map and Secret data from that namespace. -### Using Advanced Features Of Spring Vault +=== Using Advanced Features Of Spring Vault In order to use some of the https://docs.spring.io/spring-cloud-config/reference/server/environment-repository/vault-backend.html[more advanced Spring Vault features] of the **Spring Cloud Config Server**, https://mvnrepository.com/artifact/org.springframework.vault/spring-vault-core[`spring-vault-core`] must be on the classpath. By default, Spring Cloud Kubernetes can generate a Docker image for deploying Config Server to Kubernetes, but it does not include `spring-vault-core` in the classpath. If you need `spring-vault-core` to enable certain functionality in the Config Server you can build your own version of Docker image by enabling the `vault` Maven profile when running Maven build. Example: -```bash + +[source,bash] +---- $ ../../mvnw clean install -Pvault -``` +---- -### Kubernetes Access Controls +=== Kubernetes Access Controls The Kubernetes Config Server uses the Kubernetes API server to fetch Config Map and Secret data. In order for it to do that it needs ability to `get` and `list` Config Map and Secrets (depending on what you enable/disable). -## Deployment Yaml +== Deployment Yaml Below is a sample deployment, service and permissions configuration you can use to deploy a basic Config Server to Kubernetes. 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 3691e3348a..658bb1b975 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 @@ -19,6 +19,7 @@ import io.kubernetes.client.openapi.apis.CoreV1Api; import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.core.env.Environment; /** @@ -27,10 +28,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, ReadType readType) { } 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 e374898eaf..57d0240c55 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,7 @@ protected void registerBeans(ConfigDataLocationResolverContext resolverContext, coreV1Api, configMapProperties, namespaceProvider); if (isRetryEnabledForConfigMap(configMapProperties)) { configMapPropertySourceLocator = new ConfigDataRetryableConfigMapPropertySourceLocator( - configMapPropertySourceLocator, configMapProperties, new KubernetesClientConfigMapsCache()); + configMapPropertySourceLocator, configMapProperties); } registerSingle(bootstrapContext, ConfigMapPropertySourceLocator.class, configMapPropertySourceLocator, @@ -76,7 +76,7 @@ protected void registerBeans(ConfigDataLocationResolverContext resolverContext, coreV1Api, namespaceProvider, secretsProperties); if (isRetryEnabledForSecrets(secretsProperties)) { secretsPropertySourceLocator = new ConfigDataRetryableSecretsPropertySourceLocator( - secretsPropertySourceLocator, secretsProperties, new KubernetesClientSecretsCache()); + secretsPropertySourceLocator, secretsProperties); } 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 2b104952da..1949c3522f 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 @@ -22,11 +22,15 @@ import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties; import org.springframework.cloud.kubernetes.commons.config.ConfigMapPropertySourceLocator; import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.core.annotation.Order; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; import static org.springframework.cloud.kubernetes.client.KubernetesClientUtils.getApplicationNamespace; +import static org.springframework.cloud.kubernetes.client.config.KubernetesClientSourcesBatchRead.discardConfigMaps; /** * @author Ryan Baxter @@ -41,19 +45,26 @@ public class KubernetesClientConfigMapPropertySourceLocator extends ConfigMapPro public KubernetesClientConfigMapPropertySourceLocator(CoreV1Api coreV1Api, ConfigMapConfigProperties properties, KubernetesNamespaceProvider kubernetesNamespaceProvider) { - super(properties, new KubernetesClientConfigMapsCache()); + super(properties); this.coreV1Api = coreV1Api; this.kubernetesNamespaceProvider = kubernetesNamespaceProvider; } + public PropertySource locate(Environment environment) { + PropertySource propertySource = super.locate(environment); + discardConfigMaps(); + return propertySource; + } + @Override - protected MapPropertySource getMapPropertySource(NormalizedSource source, ConfigurableEnvironment environment) { + protected MapPropertySource getMapPropertySource(NormalizedSource source, ConfigurableEnvironment environment, + ReadType readType) { String normalizedNamespace = source.namespace().orElse(null); String namespace = getApplicationNamespace(normalizedNamespace, source.target(), kubernetesNamespaceProvider); KubernetesClientConfigContext context = new KubernetesClientConfigContext(coreV1Api, source, namespace, - environment); + environment, true, readType); return new KubernetesClientConfigMapPropertySource(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/KubernetesClientConfigMapsCache.java deleted file mode 100644 index 96b78d94f8..0000000000 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapsCache.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2013-present 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.concurrent.ConcurrentHashMap; - -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.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 { - - 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 strippedConfigMaps(coreV1Api.listNamespacedConfigMap(namespace).execute().getItems()); - } - catch (ApiException apiException) { - throw new RuntimeException(apiException.getResponseBody(), apiException); - } - }); - - if (b[0]) { - LOG.debug(() -> "Loaded all config maps in namespace '" + namespace + "'"); - } - else { - LOG.debug(() -> "Loaded (from cache) all config maps in namespace '" + namespace + "'"); - } - - return result; - } - - private static List strippedConfigMaps(List configMaps) { - return configMaps.stream() - .map(configMap -> new StrippedSourceContainer(configMap.getMetadata().getLabels(), - configMap.getMetadata().getName(), configMap.getData())) - .toList(); - } - -} 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 a4d629670d..e901d3be9a 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 @@ -20,30 +20,37 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; +import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.apis.CoreV1Api; -import org.apache.commons.logging.Log; +import 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.KubernetesNamespaceProvider; -import org.springframework.cloud.kubernetes.commons.config.ConfigUtils; import org.springframework.cloud.kubernetes.commons.config.MultipleSourcesContainer; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.cloud.kubernetes.commons.config.StrippedSourceContainer; import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; import org.springframework.core.env.Environment; +import org.springframework.core.log.LogAccessor; +import org.springframework.util.ObjectUtils; import static org.springframework.cloud.kubernetes.client.KubernetesClientUtils.getApplicationNamespace; +import static org.springframework.cloud.kubernetes.client.config.KubernetesClientSourcesBatchRead.strippedConfigMaps; +import static org.springframework.cloud.kubernetes.client.config.KubernetesClientSourcesBatchRead.strippedSecrets; +import static org.springframework.cloud.kubernetes.client.config.KubernetesClientSourcesSingleRead.strippedConfigMaps; +import static org.springframework.cloud.kubernetes.client.config.KubernetesClientSourcesSingleRead.strippedSecrets; +import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.processLabeledData; +import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.processNamedData; /** * @author Ryan Baxter */ public final class KubernetesClientConfigUtils { - private static final Log LOG = LogFactory.getLog(KubernetesClientConfigUtils.class); - - // k8s-native client already returns data from secrets as being decoded - // this flags makes sure we use it everywhere - private static final boolean DECODE = Boolean.FALSE; + private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(KubernetesClientConfigUtils.class)); private KubernetesClientConfigUtils() { } @@ -61,38 +68,32 @@ 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. 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 coreV1Api, String namespace, - Map labels, Environment environment) { - List strippedSecrets = strippedSecrets(coreV1Api, namespace); - if (strippedSecrets.isEmpty()) { - return MultipleSourcesContainer.empty(); - } - return ConfigUtils.processLabeledData(strippedSecrets, environment, labels, namespace, 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. see if any from (2) has a single yaml/properties file
+	 *     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) { - List strippedConfigMaps = strippedConfigMaps(coreV1Api, namespace); - if (strippedConfigMaps.isEmpty()) { - return MultipleSourcesContainer.empty(); + static MultipleSourcesContainer configMapsByName(CoreV1Api client, String namespace, + LinkedHashSet sourceNames, Environment environment, boolean includeDefaultProfileData, + ReadType readType) { + + List strippedConfigMaps; + + if (readType.equals(ReadType.BATCH)) { + LOG.debug(() -> "Will read all configmaps in namespace : " + namespace); + strippedConfigMaps = strippedConfigMaps(client, namespace); } - return ConfigUtils.processLabeledData(strippedConfigMaps, environment, labels, namespace, DECODE); + else { + LOG.debug(() -> "Will read individual configmaps in namespace : " + namespace + " with names : " + + sourceNames); + strippedConfigMaps = strippedConfigMaps(client, namespace, sourceNames); + } + + return processNamedData(strippedConfigMaps, environment, sourceNames, namespace, false, + includeDefaultProfileData); } /** @@ -103,49 +104,100 @@ 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 secretsByName(CoreV1Api client, String namespace, LinkedHashSet sourceNames, + Environment environment, boolean includeDefaultProfileData, ReadType readType) { + + List strippedSecrets; + + if (readType.equals(ReadType.BATCH)) { + LOG.debug(() -> "Will read all secrets in namespace : " + namespace); + strippedSecrets = strippedSecrets(client, namespace); } - return ConfigUtils.processNamedData(strippedSecrets, environment, sourceNames, namespace, DECODE, - includeDefaultProfileData); + else { + LOG.debug( + () -> "Will read individual secrets in namespace : " + namespace + " with names : " + sourceNames); + strippedSecrets = strippedSecrets(client, namespace, sourceNames); + } + + return 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(); + static MultipleSourcesContainer configMapsByLabels(CoreV1Api client, String namespace, Map labels, + Environment environment, ReadType readType) { + + List strippedConfigMaps; + + if (readType.equals(ReadType.BATCH)) { + LOG.debug(() -> "Will read all configmaps in namespace : " + namespace); + strippedConfigMaps = strippedConfigMaps(client, namespace); } - return ConfigUtils.processNamedData(strippedConfigMaps, environment, sourceNames, namespace, DECODE, - includeDefaultProfileData); + else { + LOG.debug(() -> "Will read individual configmaps in namespace : " + namespace + " with labels : " + labels); + strippedConfigMaps = strippedConfigMaps(client, namespace, labels); + } + + return processLabeledData(strippedConfigMaps, environment, labels, namespace, false); } - private static List strippedConfigMaps(CoreV1Api coreV1Api, String namespace) { - List strippedConfigMaps = KubernetesClientConfigMapsCache.byNamespace(coreV1Api, - namespace); - if (strippedConfigMaps.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 secretsByLabels(CoreV1Api client, String namespace, Map labels, + Environment environment, ReadType readType) { + + List strippedSecrets; + + if (readType.equals(ReadType.BATCH)) { + LOG.debug(() -> "Will read all secrets in namespace : " + namespace); + strippedSecrets = strippedSecrets(client, namespace); } - return strippedConfigMaps; + else { + LOG.debug(() -> "Will read individual secrets in namespace : " + namespace + " with labels : " + labels); + strippedSecrets = strippedSecrets(client, namespace, labels); + } + + return processLabeledData(strippedSecrets, environment, labels, namespace, false); } - private static List strippedSecrets(CoreV1Api coreV1Api, String namespace) { - List strippedSecrets = KubernetesClientSecretsCache.byNamespace(coreV1Api, namespace); - if (strippedSecrets.isEmpty()) { - LOG.debug("No secrets in namespace '" + namespace + "'"); + static List stripSecrets(List secrets) { + return secrets.stream() + .map(secret -> new StrippedSourceContainer(secret.getMetadata().getLabels(), secret.getMetadata().getName(), + transform(secret.getData()))) + .toList(); + } + + static List stripConfigMaps(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"); } - return strippedSecrets; + 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/KubernetesClientSecretsCache.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsCache.java deleted file mode 100644 index 1b13202a36..0000000000 --- a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSecretsCache.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2013-present 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).execute().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 5bacc0a170..ecbac954ac 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 @@ -20,13 +20,17 @@ import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties; import org.springframework.cloud.kubernetes.commons.config.SecretsPropertySource; import org.springframework.cloud.kubernetes.commons.config.SecretsPropertySourceLocator; import org.springframework.core.annotation.Order; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertySource; import static org.springframework.cloud.kubernetes.client.KubernetesClientUtils.getApplicationNamespace; +import static org.springframework.cloud.kubernetes.client.config.KubernetesClientSourcesBatchRead.discardSecrets; /** * @author Ryan Baxter @@ -41,19 +45,27 @@ public class KubernetesClientSecretsPropertySourceLocator extends SecretsPropert public KubernetesClientSecretsPropertySourceLocator(CoreV1Api coreV1Api, KubernetesNamespaceProvider kubernetesNamespaceProvider, SecretsConfigProperties secretsConfigProperties) { - super(secretsConfigProperties, new KubernetesClientSecretsCache()); + super(secretsConfigProperties); this.coreV1Api = coreV1Api; this.kubernetesNamespaceProvider = kubernetesNamespaceProvider; } @Override - protected SecretsPropertySource getPropertySource(ConfigurableEnvironment environment, NormalizedSource source) { + public PropertySource locate(Environment environment) { + PropertySource propertySource = super.locate(environment); + discardSecrets(); + return propertySource; + } + + @Override + protected SecretsPropertySource getPropertySource(ConfigurableEnvironment environment, NormalizedSource source, + ReadType readType) { String normalizedNamespace = source.namespace().orElse(null); String namespace = getApplicationNamespace(normalizedNamespace, source.target(), kubernetesNamespaceProvider); KubernetesClientConfigContext context = new KubernetesClientConfigContext(coreV1Api, source, namespace, - environment); + environment, true, readType); return new KubernetesClientSecretsPropertySource(context); } diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSourcesBatchRead.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSourcesBatchRead.java new file mode 100644 index 0000000000..48b6f2df11 --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSourcesBatchRead.java @@ -0,0 +1,80 @@ +/* + * Copyright 2013-present 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.concurrent.ConcurrentHashMap; + +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.apis.CoreV1Api; + +import org.springframework.cloud.kubernetes.commons.config.StrippedSourceContainer; + +import static org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigUtils.stripConfigMaps; +import static org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigUtils.stripSecrets; + +/** + * A cache of ConfigMaps / Secrets per namespace. Makes sure we read only once from a + * namespace. + * + * @author wind57 + */ +public final class KubernetesClientSourcesBatchRead { + + private KubernetesClientSourcesBatchRead() { + + } + + /** + * 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> SECRETS_CACHE = new ConcurrentHashMap<>(); + + private static final ConcurrentHashMap> CONFIGMAPS_CACHE = new ConcurrentHashMap<>(); + + public static void discardSecrets() { + SECRETS_CACHE.clear(); + } + + public static void discardConfigMaps() { + CONFIGMAPS_CACHE.clear(); + } + + static List strippedConfigMaps(CoreV1Api coreV1Api, String namespace) { + return CONFIGMAPS_CACHE.computeIfAbsent(namespace, x -> { + try { + return stripConfigMaps(coreV1Api.listNamespacedConfigMap(namespace).execute().getItems()); + } + catch (ApiException apiException) { + throw new RuntimeException(apiException.getResponseBody(), apiException); + } + }); + } + + static List strippedSecrets(CoreV1Api coreV1Api, String namespace) { + return SECRETS_CACHE.computeIfAbsent(namespace, x -> { + try { + return stripSecrets(coreV1Api.listNamespacedSecret(namespace).execute().getItems()); + } + catch (ApiException apiException) { + throw new RuntimeException(apiException.getResponseBody(), apiException); + } + }); + } + +} diff --git a/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSourcesSingleRead.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSourcesSingleRead.java new file mode 100644 index 0000000000..dceac6047f --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientSourcesSingleRead.java @@ -0,0 +1,165 @@ +/* + * Copyright 2013-present 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; + +import static org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigUtils.handleApiException; +import static org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigUtils.stripConfigMaps; +import static org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigUtils.stripSecrets; + +final class KubernetesClientSourcesSingleRead { + + private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(KubernetesClientSourcesSingleRead.class)); + + private KubernetesClientSourcesSingleRead() { + + } + + /** + * read configmaps by name, one by one, without caching them. + */ + static List strippedConfigMaps(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).execute(); + } + catch (ApiException e) { + handleApiException(e, sourceName); + } + if (configMap != null) { + LOG.debug("Loaded config map '" + sourceName + "'"); + configMaps.add(configMap); + } + } + + List strippedConfigMaps = stripConfigMaps(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 strippedSecrets(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).execute(); + } + catch (ApiException e) { + handleApiException(e, sourceName); + } + if (secret != null) { + LOG.debug(() -> "Loaded config map '" + sourceName + "'"); + secrets.add(secret); + } + } + + List strippedSecrets = stripSecrets(secrets); + + if (strippedSecrets.isEmpty()) { + LOG.debug(() -> "No secrets in namespace '" + namespace + "'"); + } + + return strippedSecrets; + } + + /** + * read configmaps by labels, without caching them. + */ + static List strippedConfigMaps(CoreV1Api client, String namespace, + Map labels) { + + List configMaps; + try { + configMaps = client.listNamespacedConfigMap(namespace) + .labelSelector(labelSelector(labels)) + .execute() + .getItems(); + } + catch (ApiException e) { + throw new RuntimeException(e.getResponseBody(), e); + } + for (V1ConfigMap configMap : configMaps) { + LOG.debug(() -> "Loaded config map '" + configMap.getMetadata().getName() + "'"); + } + + List strippedConfigMaps = stripConfigMaps(configMaps); + if (strippedConfigMaps.isEmpty()) { + LOG.debug(() -> "No configmaps in namespace '" + namespace + "'"); + } + + return strippedConfigMaps; + } + + /** + * read secrets by labels, without caching them. + */ + static List strippedSecrets(CoreV1Api client, String namespace, + Map labels) { + + List secrets; + try { + secrets = client.listNamespacedSecret(namespace).labelSelector(labelSelector(labels)).execute().getItems(); + } + catch (ApiException e) { + throw new RuntimeException(e.getResponseBody(), e); + } + for (V1Secret secret : secrets) { + LOG.debug(() -> "Loaded secret '" + secret.getMetadata().getName() + "'"); + } + + List strippedSecrets = stripSecrets(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/LabeledConfigMapContextToSourceDataProvider.java b/spring-cloud-kubernetes-client-config/src/main/java/org/springframework/cloud/kubernetes/client/config/LabeledConfigMapContextToSourceDataProvider.java index c5eb046c9c..32c48c6626 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 @@ -23,6 +23,8 @@ import org.springframework.cloud.kubernetes.commons.config.LabeledSourceData; import org.springframework.cloud.kubernetes.commons.config.MultipleSourcesContainer; +import static org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigUtils.configMapsByLabels; + class LabeledConfigMapContextToSourceDataProvider implements Supplier { LabeledConfigMapContextToSourceDataProvider() { @@ -48,8 +50,8 @@ public KubernetesClientContextToSourceData get() { return new LabeledSourceData() { @Override public MultipleSourcesContainer dataSupplier(Map labels) { - return KubernetesClientConfigUtils.configMapsDataByLabels(context.client(), context.namespace(), - labels, context.environment()); + return configMapsByLabels(context.client(), context.namespace(), labels, context.environment(), + context.readType()); } }.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 2956989958..d542c7faae 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 @@ -23,6 +23,8 @@ import org.springframework.cloud.kubernetes.commons.config.LabeledSourceData; import org.springframework.cloud.kubernetes.commons.config.MultipleSourcesContainer; +import static org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigUtils.secretsByLabels; + /** * Provides an implementation of {@link KubernetesClientContextToSourceData} for a labeled * secret. @@ -55,8 +57,8 @@ public KubernetesClientContextToSourceData get() { return new LabeledSourceData() { @Override public MultipleSourcesContainer dataSupplier(Map labels) { - return KubernetesClientConfigUtils.secretsDataByLabels(context.client(), context.namespace(), - labels, context.environment()); + return secretsByLabels(context.client(), context.namespace(), labels, context.environment(), + context.readType()); } }.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 198a1f361a..c15736d158 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 @@ -24,6 +24,8 @@ import org.springframework.cloud.kubernetes.commons.config.NamedConfigMapNormalizedSource; import org.springframework.cloud.kubernetes.commons.config.NamedSourceData; +import static org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigUtils.configMapsByName; + /** * Provides an implementation of {@link KubernetesClientContextToSourceData} for a named * config map. @@ -54,8 +56,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()); + return configMapsByName(context.client(), context.namespace(), sourceNames, context.environment(), + context.includeDefaultProfileData(), context.readType()); } }.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 793b7cf7d2..7642a352c1 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 @@ -24,6 +24,8 @@ import org.springframework.cloud.kubernetes.commons.config.NamedSecretNormalizedSource; import org.springframework.cloud.kubernetes.commons.config.NamedSourceData; +import static org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigUtils.secretsByName; + /** * Provides an implementation of {@link KubernetesClientContextToSourceData} for a named * secret. @@ -53,8 +55,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()); + return secretsByName(context.client(), context.namespace(), sourceNames, context.environment(), + context.includeDefaultProfileData(), context.readType()); } }.compute(source.name().orElseThrow(), source.prefix(), source.target(), source.profileSpecificSources(), source.failFast(), context.namespace(), context.environment().getActiveProfiles()); diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapErrorOnReadingSourceTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapErrorOnReadingSourceTests.java index f6bb1c3257..3cc072fb5a 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapErrorOnReadingSourceTests.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapErrorOnReadingSourceTests.java @@ -39,6 +39,7 @@ import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.cloud.kubernetes.commons.config.RetryProperties; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.PropertySource; @@ -110,7 +111,7 @@ void namedSingleConfigMapFails(CapturedOutput output) { stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(), - Map.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT); + Map.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT, ReadType.BATCH); CoreV1Api api = new CoreV1Api(); KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api, @@ -153,7 +154,7 @@ void namedTwoConfigMapsOneFails(CapturedOutput output) { ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(sourceOne, sourceTwo), Map.of(), true, null, namespace, false, true, false, - RetryProperties.DEFAULT); + RetryProperties.DEFAULT, ReadType.BATCH); CoreV1Api api = new CoreV1Api(); KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api, @@ -200,7 +201,7 @@ void namedTwoConfigMapsBothFail(CapturedOutput output) { ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(sourceOne, sourceTwo), Map.of(), true, null, namespace, false, true, false, - RetryProperties.DEFAULT); + RetryProperties.DEFAULT, ReadType.BATCH); CoreV1Api api = new CoreV1Api(); KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api, @@ -243,7 +244,8 @@ void labeledSingleConfigMapFails(CapturedOutput output) { null, null, null); ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), - List.of(configMapSource), labels, true, null, namespace, false, true, false, RetryProperties.DEFAULT); + List.of(configMapSource), labels, true, null, namespace, false, true, false, RetryProperties.DEFAULT, + ReadType.BATCH); CoreV1Api api = new CoreV1Api(); KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api, @@ -295,7 +297,7 @@ void labeledTwoConfigMapsOneFails(CapturedOutput output) { ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(sourceOne, sourceTwo), Map.of("one", "1", "two", "2"), true, null, namespace, false, true, - false, RetryProperties.DEFAULT); + false, RetryProperties.DEFAULT, ReadType.BATCH); CoreV1Api api = new CoreV1Api(); KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api, @@ -349,7 +351,7 @@ void labeledTwoConfigMapsBothFail(CapturedOutput output) { ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(sourceOne, sourceTwo), Map.of("one", "1", "two", "2"), true, null, namespace, false, true, - false, RetryProperties.DEFAULT); + false, RetryProperties.DEFAULT, ReadType.BATCH); CoreV1Api api = new CoreV1Api(); KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api, diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocatorTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/KubernetesClientConfigMapPropertySourceLocatorTests.java index 0a3c83cd7c..cc6e2059e5 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 @@ -43,6 +43,7 @@ import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties; import org.springframework.cloud.kubernetes.commons.config.Constants; import org.springframework.cloud.kubernetes.commons.config.NamespaceResolutionFailedException; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.cloud.kubernetes.commons.config.RetryProperties; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.PropertySource; @@ -104,9 +105,9 @@ public void afterEach() { void locateWithoutSources() { CoreV1Api api = new CoreV1Api(); stubFor(get("/api/v1/namespaces/default/configmaps") - .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(PROPERTIES_CONFIGMAP_LIST)))); + .willReturn(aResponse().withStatus(200).withBody(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, ReadType.BATCH); MockEnvironment mockEnvironment = new MockEnvironment(); mockEnvironment.setProperty("spring.cloud.kubernetes.client.namespace", "default"); PropertySource propertySource = new KubernetesClientConfigMapPropertySourceLocator(api, @@ -120,12 +121,13 @@ configMapConfigProperties, new KubernetesNamespaceProvider(mockEnvironment)) void locateWithSources() { CoreV1Api api = new CoreV1Api(); stubFor(get("/api/v1/namespaces/default/configmaps") - .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(PROPERTIES_CONFIGMAP_LIST)))); + .willReturn(aResponse().withStatus(200).withBody(JSON.serialize(PROPERTIES_CONFIGMAP_LIST)))); 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, + ReadType.BATCH); PropertySource propertySource = new KubernetesClientConfigMapPropertySourceLocator(api, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())) @@ -145,10 +147,10 @@ configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment() void testLocateWithoutNamespaceConstructor() { CoreV1Api api = new CoreV1Api(); stubFor(get("/api/v1/namespaces/default/configmaps") - .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(PROPERTIES_CONFIGMAP_LIST)))); + .willReturn(aResponse().withStatus(200).withBody(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, ReadType.BATCH); assertThatThrownBy(() -> new KubernetesClientConfigMapPropertySourceLocator(api, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())) @@ -166,9 +168,9 @@ void testLocateWithoutNamespaceConstructor() { void testLocateWithoutNamespace() { CoreV1Api api = new CoreV1Api(); stubFor(get("/api/v1/namespaces/default/configmaps") - .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(PROPERTIES_CONFIGMAP_LIST)))); + .willReturn(aResponse().withStatus(200).withBody(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, ReadType.BATCH); assertThatThrownBy(() -> new KubernetesClientConfigMapPropertySourceLocator(api, configMapConfigProperties, new KubernetesNamespaceProvider(ENV)) .locate(ENV)).isInstanceOf(NamespaceResolutionFailedException.class); @@ -181,7 +183,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, + ReadType.BATCH); KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -197,7 +200,8 @@ public void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled(Capture .willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(), - Map.of(), true, "bootstrap-640", "default", false, false, false, RetryProperties.DEFAULT); + Map.of(), true, "bootstrap-640", "default", false, false, false, RetryProperties.DEFAULT, + ReadType.BATCH); 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 3def13e49b..2f044f2cc2 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 @@ -35,6 +35,7 @@ import org.springframework.cloud.kubernetes.commons.config.Constants; import org.springframework.cloud.kubernetes.commons.config.NamedConfigMapNormalizedSource; import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.mock.env.MockEnvironment; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; @@ -97,18 +98,18 @@ public static void after() { @AfterEach public void afterEach() { WireMock.reset(); - new KubernetesClientConfigMapsCache().discardAll(); + KubernetesClientSourcesBatchRead.discardConfigMaps(); } @Test public void propertiesFile() { CoreV1Api api = new CoreV1Api(); stubFor(get("/api/v1/namespaces/default/configmaps") - .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(PROPERTIES_CONFIGMAP_LIST)))); + .willReturn(aResponse().withStatus(200).withBody(JSON.serialize(PROPERTIES_CONFIGMAP_LIST)))); NormalizedSource source = new NamedConfigMapNormalizedSource("bootstrap-640", "default", false, true); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, "default", - new MockEnvironment()); + new MockEnvironment(), false, ReadType.BATCH); KubernetesClientConfigMapPropertySource propertySource = new KubernetesClientConfigMapPropertySource(context); verify(getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps"))); @@ -125,11 +126,11 @@ public void propertiesFile() { public void yamlFile() { CoreV1Api api = new CoreV1Api(); stubFor(get("/api/v1/namespaces/default/configmaps") - .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(YAML_CONFIGMAP_LIST)))); + .willReturn(aResponse().withStatus(200).withBody(JSON.serialize(YAML_CONFIGMAP_LIST)))); NormalizedSource source = new NamedConfigMapNormalizedSource("bootstrap-641", "default", false, true); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, "default", - new MockEnvironment()); + new MockEnvironment(), false, ReadType.BATCH); KubernetesClientConfigMapPropertySource propertySource = new KubernetesClientConfigMapPropertySource(context); verify(getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps"))); @@ -146,12 +147,12 @@ public void yamlFile() { public void propertiesFileWithPrefix() { CoreV1Api api = new CoreV1Api(); stubFor(get("/api/v1/namespaces/default/configmaps") - .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(PROPERTIES_CONFIGMAP_LIST)))); + .willReturn(aResponse().withStatus(200).withBody(JSON.serialize(PROPERTIES_CONFIGMAP_LIST)))); 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, ReadType.BATCH); KubernetesClientConfigMapPropertySource propertySource = new KubernetesClientConfigMapPropertySource(context); verify(getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps"))); @@ -171,7 +172,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, ReadType.BATCH); assertThat(new KubernetesClientConfigMapPropertySource(context)).isNotNull(); } @@ -184,7 +185,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, ReadType.BATCH); assertThatThrownBy(() -> new KubernetesClientConfigMapPropertySource(context)) .isInstanceOf(IllegalStateException.class) @@ -200,7 +201,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, ReadType.BATCH); 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 244f51ed68..e26cf30ed6 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 @@ -37,6 +37,7 @@ import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.config.NamespaceResolutionFailedException; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.cloud.kubernetes.commons.config.RetryProperties; import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties; import org.springframework.core.env.CompositePropertySource; @@ -147,7 +148,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, + ReadType.BATCH); PropertySource propertySource = new KubernetesClientSecretsPropertySourceLocator(api, new KubernetesNamespaceProvider(new MockEnvironment()), secretsConfigProperties) @@ -161,7 +163,7 @@ 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, ReadType.BATCH); PropertySource propertySource = new KubernetesClientSecretsPropertySourceLocator(api, new KubernetesNamespaceProvider(new MockEnvironment()), secretsConfigProperties) @@ -183,7 +185,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, ReadType.BATCH); assertThatThrownBy(() -> new KubernetesClientSecretsPropertySourceLocator(api, new KubernetesNamespaceProvider(new MockEnvironment()), secretsConfigProperties) @@ -196,7 +198,7 @@ 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, ReadType.BATCH); KubernetesClientSecretsPropertySourceLocator locator = new KubernetesClientSecretsPropertySourceLocator(api, new KubernetesNamespaceProvider(new MockEnvironment()), secretsConfigProperties); @@ -211,7 +213,7 @@ void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled(CapturedOutput stubFor(get(LIST_API).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), - List.of(), true, "db-secret", "default", false, true, false, RetryProperties.DEFAULT); + List.of(), true, "db-secret", "default", false, true, false, RetryProperties.DEFAULT, ReadType.BATCH); 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 7c78f48d65..1895ee588f 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 @@ -39,6 +39,7 @@ import org.springframework.cloud.kubernetes.commons.config.LabeledSecretNormalizedSource; import org.springframework.cloud.kubernetes.commons.config.NamedSecretNormalizedSource; import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.mock.env.MockEnvironment; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; @@ -153,18 +154,17 @@ static void after() { @AfterEach void afterEach() { WireMock.reset(); - new KubernetesClientSecretsCache().discardAll(); + KubernetesClientSourcesBatchRead.discardSecrets(); } @Test void emptyDataSecretTest() { CoreV1Api api = new CoreV1Api(); - stubFor(get(API) - .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(EMPTY_DATA_SECRET_LIST)))); + stubFor(get(API).willReturn(aResponse().withStatus(200).withBody(JSON.serialize(EMPTY_DATA_SECRET_LIST)))); NormalizedSource source = new NamedSecretNormalizedSource("db-secret", "default", false, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, "default", - new MockEnvironment()); + new MockEnvironment(), true, ReadType.BATCH); KubernetesClientSecretsPropertySource propertySource = new KubernetesClientSecretsPropertySource(context); assertThat(propertySource.getName()).isEqualTo("secret.db-secret.default"); @@ -174,11 +174,11 @@ void emptyDataSecretTest() { @Test void secretsTest() { CoreV1Api api = new CoreV1Api(); - stubFor(get(API).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(SECRET_LIST)))); + stubFor(get(API).willReturn(aResponse().withStatus(200).withBody(JSON.serialize(SECRET_LIST)))); NormalizedSource source = new NamedSecretNormalizedSource("db-secret", "default", false, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, "default", - new MockEnvironment()); + new MockEnvironment(), true, ReadType.BATCH); KubernetesClientSecretsPropertySource propertySource = new KubernetesClientSecretsPropertySource(context); assertThat(propertySource.containsProperty("password")).isTrue(); @@ -197,7 +197,7 @@ void secretLabelsTest() { NormalizedSource source = new LabeledSecretNormalizedSource("default", labels, false, ConfigUtils.Prefix.DEFAULT); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, "default", - new MockEnvironment()); + new MockEnvironment(), false, ReadType.BATCH); KubernetesClientSecretsPropertySource propertySource = new KubernetesClientSecretsPropertySource(context); assertThat(propertySource.containsProperty("spring.rabbitmq.password")).isTrue(); @@ -211,7 +211,7 @@ void constructorShouldThrowExceptionOnFailureWhenFailFastIsEnabled() { NormalizedSource source = new NamedSecretNormalizedSource("secret", "default", true, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, "default", - new MockEnvironment()); + new MockEnvironment(), true, ReadType.BATCH); assertThatThrownBy(() -> new KubernetesClientSecretsPropertySource(context)) .isInstanceOf(IllegalStateException.class) @@ -226,7 +226,7 @@ void constructorShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() { NormalizedSource source = new NamedSecretNormalizedSource("secret", "db-secret", false, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, "default", - new MockEnvironment()); + new MockEnvironment(), true, ReadType.BATCH); 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/LabeledConfigMapContextToSourceDataProviderBatchReadTests.java similarity index 93% 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/LabeledConfigMapContextToSourceDataProviderBatchReadTests.java index 9a561c0105..648ed7fd31 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/LabeledConfigMapContextToSourceDataProviderBatchReadTests.java @@ -32,30 +32,31 @@ import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; import io.kubernetes.client.util.ClientBuilder; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.test.system.CapturedOutput; -import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.config.ConfigUtils; import org.springframework.cloud.kubernetes.commons.config.LabeledConfigMapNormalizedSource; import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.ReadType; 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.getRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; import static org.assertj.core.api.Assertions.assertThat; /** * @author wind57 */ -@ExtendWith(OutputCaptureExtension.class) -class LabeledConfigMapContextToSourceDataProviderTests { +class LabeledConfigMapContextToSourceDataProviderBatchReadTests { private static final Map LABELS = new LinkedHashMap<>(); @@ -67,13 +68,12 @@ class LabeledConfigMapContextToSourceDataProviderTests { private static final String NAMESPACE = "default"; - static { + @BeforeAll + static void setup() { + LABELS.put("label2", "value2"); LABELS.put("label1", "value1"); - } - @BeforeAll - static void setup() { WireMockServer wireMockServer = new WireMockServer(options().dynamicPort()); wireMockServer.start(); @@ -87,7 +87,12 @@ static void setup() { @AfterEach void afterEach() { WireMock.reset(); - new KubernetesClientConfigMapsCache().discardAll(); + KubernetesClientSourcesBatchRead.discardConfigMaps(); + } + + @AfterAll + static void afterAll() { + WireMock.shutdownServer(); } /** @@ -111,7 +116,7 @@ void singleConfigMapMatchAgainstLabels() { NormalizedSource source = new LabeledConfigMapNormalizedSource(NAMESPACE, LABELS, true, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, ReadType.BATCH); KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -161,7 +166,7 @@ void twoConfigMapsMatchAgainstLabels() { NormalizedSource source = new LabeledConfigMapNormalizedSource(NAMESPACE, RED_LABEL, true, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, ReadType.BATCH); KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -193,7 +198,7 @@ void configMapNoMatch() { NormalizedSource source = new LabeledConfigMapNormalizedSource(NAMESPACE, BLUE_LABEL, true, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, ReadType.BATCH); KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -226,7 +231,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, ReadType.BATCH); KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -258,7 +263,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, ReadType.BATCH); KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -304,7 +309,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, ReadType.BATCH); KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -357,7 +362,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, ReadType.BATCH); KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -396,7 +401,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, ReadType.BATCH); KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -436,11 +441,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, ReadType.BATCH); KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -516,7 +521,8 @@ void searchWithLabelsTwoConfigMapsFoundAndOneFromProfileFound() { 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, ReadType.BATCH); KubernetesClientContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -542,7 +548,7 @@ void searchWithLabelsTwoConfigMapsFoundAndOneFromProfileFound() { * */ @Test - void cache(CapturedOutput output) { + void cache() { V1ConfigMap red = new V1ConfigMapBuilder() .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "red")) .withNamespace(NAMESPACE) @@ -567,19 +573,18 @@ 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, ReadType.BATCH); KubernetesClientContextToSourceData redData = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData redSourceData = redData.apply(redContext); Assertions.assertThat(redSourceData.sourceData().size()).isEqualTo(1); Assertions.assertThat(redSourceData.sourceData().get("color")).isEqualTo("red"); Assertions.assertThat(redSourceData.sourceName()).isEqualTo("configmap.red-configmap.default"); - Assertions.assertThat(output.getAll()).contains("Loaded all config maps in namespace '" + 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, ReadType.BATCH); KubernetesClientContextToSourceData greenData = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData greenSourceData = greenData.apply(greenContext); @@ -587,18 +592,14 @@ void cache(CapturedOutput output) { Assertions.assertThat(greenSourceData.sourceData().get("color")).isEqualTo("green"); Assertions.assertThat(greenSourceData.sourceName()).isEqualTo("configmap.green-configmap.default"); - // meaning there is a single entry with such a log statement - String[] out = output.getAll().split("Loaded all config maps in namespace"); - Assertions.assertThat(out.length).isEqualTo(2); + // called only once, since the use caching + verify(1, getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps"))); - // meaning that the second read was done from the cache - out = output.getAll().split("Loaded \\(from cache\\) all config maps in namespace"); - Assertions.assertThat(out.length).isEqualTo(2); } private void stubCall(V1ConfigMapList list) { stubFor(get("/api/v1/namespaces/default/configmaps") - .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(list)))); + .willReturn(aResponse().withStatus(200).withBody(JSON.serialize(list)))); } } 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/LabeledSecretContextToSourceDataProviderBatchReadTests.java similarity index 93% 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/LabeledSecretContextToSourceDataProviderBatchReadTests.java index d24dd59547..0deacfbe69 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/LabeledSecretContextToSourceDataProviderBatchReadTests.java @@ -34,30 +34,31 @@ import io.kubernetes.client.openapi.models.V1SecretList; import io.kubernetes.client.util.ClientBuilder; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.test.system.CapturedOutput; -import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.config.ConfigUtils; import org.springframework.cloud.kubernetes.commons.config.LabeledSecretNormalizedSource; import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.ReadType; 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.getRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; import static org.assertj.core.api.Assertions.assertThat; /** * @author wind57 */ -@ExtendWith(OutputCaptureExtension.class) -class LabeledSecretContextToSourceDataProviderTests { +class LabeledSecretContextToSourceDataProviderBatchReadTests { private static final Map LABELS = new LinkedHashMap<>(); @@ -85,7 +86,12 @@ static void setup() { @AfterEach void afterEach() { WireMock.reset(); - new KubernetesClientSecretsCache().discardAll(); + KubernetesClientSourcesBatchRead.discardSecrets(); + } + + @AfterAll + static void afterAll() { + WireMock.shutdownServer(); } /** @@ -110,7 +116,7 @@ void noMatch() { NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, Collections.singletonMap("color", "blue"), false, ConfigUtils.Prefix.DEFAULT); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, ReadType.BATCH); KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -139,7 +145,7 @@ void singleSecretMatchAgainstLabels() { NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, LABELS, false, ConfigUtils.Prefix.DEFAULT); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, ReadType.BATCH); KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -172,7 +178,7 @@ void twoSecretsMatchAgainstLabels() { NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, RED_LABEL, false, ConfigUtils.Prefix.DEFAULT); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, ReadType.BATCH); KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -198,7 +204,7 @@ void namespaceMatch() { NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE + "nope", LABELS, false, ConfigUtils.Prefix.DEFAULT); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, ReadType.BATCH); KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -231,7 +237,7 @@ void testWithPrefix() { 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()); + new MockEnvironment(), false, ReadType.BATCH); KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -276,7 +282,7 @@ void testTwoSecretsWithPrefix() { NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "blue"), false, ConfigUtils.Prefix.DELAYED); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, ReadType.BATCH); KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -335,7 +341,7 @@ void searchWithLabelsOneSecretFound() { NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "blue"), false, ConfigUtils.Prefix.DEFAULT); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, ReadType.BATCH); KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -348,7 +354,7 @@ 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 + * "color-secret-k8s" with label: "{color:blue}". We search by "{color:blue}" and find * both. */ @Test @@ -375,11 +381,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); - KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + false, ReadType.BATCH); KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -452,11 +458,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); - KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + false, ReadType.BATCH); KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -493,7 +499,7 @@ void testYaml() { NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "blue"), false, ConfigUtils.Prefix.DEFAULT); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, ReadType.BATCH); KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -513,7 +519,7 @@ void testYaml() { * */ @Test - void cache(CapturedOutput output) { + void cache() { V1Secret red = new V1SecretBuilder() .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "red")) .withNamespace(NAMESPACE) @@ -538,19 +544,18 @@ void cache(CapturedOutput output) { NormalizedSource redSource = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "red"), false, ConfigUtils.Prefix.DEFAULT); KubernetesClientConfigContext redContext = new KubernetesClientConfigContext(api, redSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, ReadType.BATCH); KubernetesClientContextToSourceData redData = new LabeledSecretContextToSourceDataProvider().get(); SourceData redSourceData = redData.apply(redContext); Assertions.assertThat(redSourceData.sourceData().size()).isEqualTo(1); Assertions.assertThat(redSourceData.sourceData().get("color")).isEqualTo("red"); Assertions.assertThat(redSourceData.sourceName()).isEqualTo("secret.red.default"); - Assertions.assertThat(output.getOut()).contains("Loaded all secrets in namespace '" + NAMESPACE + "'"); NormalizedSource greenSource = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "green"), false, ConfigUtils.Prefix.DEFAULT); KubernetesClientConfigContext greenContext = new KubernetesClientConfigContext(api, greenSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, ReadType.BATCH); KubernetesClientContextToSourceData greenData = new LabeledSecretContextToSourceDataProvider().get(); SourceData greenSourceData = greenData.apply(greenContext); @@ -558,18 +563,14 @@ void cache(CapturedOutput output) { Assertions.assertThat(greenSourceData.sourceData().get("color")).isEqualTo("green"); Assertions.assertThat(greenSourceData.sourceName()).isEqualTo("secret.green.default"); - // meaning there is a single entry with such a log statement - String[] out = output.getAll().split("Loaded all secrets in namespace"); - Assertions.assertThat(out.length).isEqualTo(2); + // called only once, since the use caching + verify(1, getRequestedFor(urlEqualTo("/api/v1/namespaces/default/secrets"))); - // meaning that the second read was done from the cache - out = output.getAll().split("Loaded \\(from cache\\) all secrets in namespace"); - Assertions.assertThat(out.length).isEqualTo(2); } private void stubCall(V1SecretList list) { stubFor(get("/api/v1/namespaces/default/secrets") - .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(list)))); + .willReturn(aResponse().withStatus(200).withBody(JSON.serialize(list)))); } } diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/LabeledSecretContextToSourceDataProviderSingleReadTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/LabeledSecretContextToSourceDataProviderSingleReadTests.java new file mode 100644 index 0000000000..55514858da --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/LabeledSecretContextToSourceDataProviderSingleReadTests.java @@ -0,0 +1,581 @@ +/* + * Copyright 2013-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client.config; + +import java.util.Base64; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.Configuration; +import io.kubernetes.client.openapi.JSON; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.openapi.models.V1Secret; +import io.kubernetes.client.openapi.models.V1SecretBuilder; +import io.kubernetes.client.openapi.models.V1SecretList; +import io.kubernetes.client.util.ClientBuilder; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.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.ReadType; +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.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; + +/** + * @author wind57 + */ +class LabeledSecretContextToSourceDataProviderSingleReadTests { + + private static final Map LABELS = new LinkedHashMap<>(); + + private static final Map RED_LABEL = Map.of("color", "red"); + + private static final String NAMESPACE = "default"; + + @BeforeAll + static void setup() { + + LABELS.put("label2", "value2"); + LABELS.put("label1", "value1"); + + 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(); + KubernetesClientSourcesBatchRead.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, ConfigUtils.Prefix.DEFAULT); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.color.default"); + Assertions.assertThat(sourceData.sourceData()).isEmpty(); + + } + + /** + * we have a single secret deployed. it has two labels and these match against our + * queries. + */ + @Test + void singleSecretMatchAgainstLabels() { + + V1Secret red = new V1SecretBuilder().withMetadata( + new V1ObjectMetaBuilder().withLabels(LABELS).withNamespace(NAMESPACE).withName("test-secret").build()) + .addToData("color", "really-red".getBytes()) + .build(); + V1SecretList secretList = new V1SecretList().addItemsItem(red); + + stubCall(secretList, "/api/v1/namespaces/default/secrets?labelSelector=label2%3Dvalue2%26label1%3Dvalue1"); + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, LABELS, false, + ConfigUtils.Prefix.DEFAULT); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.test-secret.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("color", "really-red")); + + } + + /** + * we have two secrets deployed. both of them have labels that match (color=red). + */ + @Test + void twoSecretsMatchAgainstLabels() { + + V1Secret one = new V1SecretBuilder().withMetadata( + new V1ObjectMetaBuilder().withLabels(RED_LABEL).withNamespace(NAMESPACE).withName("color-one").build()) + .addToData("colorOne", "really-red-one".getBytes()) + .build(); + + V1Secret two = new V1SecretBuilder().withMetadata( + new V1ObjectMetaBuilder().withLabels(RED_LABEL).withNamespace(NAMESPACE).withName("color-two").build()) + .addToData("colorTwo", "really-red-two".getBytes()) + .build(); + + V1SecretList secretList = new V1SecretList().addItemsItem(one).addItemsItem(two); + stubCall(secretList, "/api/v1/namespaces/default/secrets?labelSelector=color%3Dred"); + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, RED_LABEL, false, + ConfigUtils.Prefix.DEFAULT); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.color-one.color-two.default"); + Assertions.assertThat(sourceData.sourceData().size()).isEqualTo(2); + Assertions.assertThat(sourceData.sourceData().get("colorOne")).isEqualTo("really-red-one"); + Assertions.assertThat(sourceData.sourceData().get("colorTwo")).isEqualTo("really-red-two"); + + } + + @Test + void namespaceMatch() { + V1Secret one = new V1SecretBuilder().withMetadata( + new V1ObjectMetaBuilder().withLabels(LABELS).withNamespace(NAMESPACE).withName("test-secret").build()) + .addToData("color", "really-red".getBytes()) + .build(); + V1SecretList secretList = new V1SecretList().addItemsItem(one); + + stubCall(secretList, "/api/v1/namespaces/default/secrets?labelSelector=label2%3Dvalue2%26label1%3Dvalue1"); + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE + "nope", LABELS, false, + ConfigUtils.Prefix.DEFAULT); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.test-secret.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("color", "really-red")); + } + + /** + * one secret with name : "blue-secret" and labels "color=blue" is deployed. we search + * it with the same labels, find it, and assert that name of the SourceData (it must + * use its name, not its labels) and values in the SourceData must be prefixed (since + * we have provided an explicit prefix). + */ + @Test + void testWithPrefix() { + + V1Secret one = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue")) + .withNamespace(NAMESPACE) + .withName("blue-secret") + .build()) + .addToData("what-color", "blue-color".getBytes()) + .build(); + V1SecretList secretList = new V1SecretList().addItemsItem(one); + + stubCall(secretList, "/api/v1/namespaces/default/secrets?labelSelector=color%3Dblue"); + CoreV1Api api = new CoreV1Api(); + + ConfigUtils.Prefix prefix = ConfigUtils.findPrefix("me", false, false, null); + NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "blue"), false, prefix); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.blue-secret.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("me.what-color", "blue-color")); + } + + /** + * two secrets are deployed (name:blue-secret, name:another-blue-secret) and labels + * "color=blue" (on both). we search with the same labels, find them, and assert that + * name of the SourceData (it must use its name, not its labels) and values in the + * SourceData must be prefixed (since we have provided a delayed prefix). + * + * Also notice that the prefix is made up from both secret names. + * + */ + @Test + void testTwoSecretsWithPrefix() { + + V1Secret one = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue")) + .withNamespace(NAMESPACE) + .withName("blue-secret") + .build()) + .addToData("first", "blue".getBytes()) + .build(); + + V1Secret two = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue")) + .withNamespace(NAMESPACE) + .withName("another-blue-secret") + .build()) + .addToData("second", "blue".getBytes()) + .build(); + + V1SecretList secretList = new V1SecretList().addItemsItem(one).addItemsItem(two); + + stubCall(secretList, "/api/v1/namespaces/default/secrets?labelSelector=color%3Dblue"); + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "blue"), false, + ConfigUtils.Prefix.DELAYED); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + // maps don't have a defined order, so assert components separately + Assertions.assertThat(sourceData.sourceName().length()).isEqualTo(46); + Assertions.assertThat(sourceData.sourceName()).contains("secret"); + Assertions.assertThat(sourceData.sourceName()).contains("blue-secret"); + Assertions.assertThat(sourceData.sourceName()).contains("another-blue-secret"); + Assertions.assertThat(sourceData.sourceName()).contains("default"); + + Map properties = sourceData.sourceData(); + Assertions.assertThat(properties).hasSize(2); + Iterator keys = properties.keySet().iterator(); + String firstKey = keys.next(); + String secondKey = keys.next(); + + if (firstKey.contains("first")) { + Assertions.assertThat(firstKey).isEqualTo("blue-secret.first"); + } + + Assertions.assertThat(secondKey).isEqualTo("another-blue-secret.second"); + Assertions.assertThat(properties.get(firstKey)).isEqualTo("blue"); + Assertions.assertThat(properties.get(secondKey)).isEqualTo("blue"); + } + + /** + * two secrets are deployed: secret "color-secret" with label: "{color:blue}" and + * "shape-secret" with label: "{shape:round}". We search by "{color:blue}" and find + * one secret. + */ + @Test + void searchWithLabelsOneSecretFound() { + + V1Secret colorSecret = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue")) + .withNamespace(NAMESPACE) + .withName("color-secret") + .build()) + .addToData("one", "1".getBytes()) + .build(); + + V1Secret shapeSecret = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("shape", "round")) + .withNamespace(NAMESPACE) + .withName("shape-secret") + .build()) + .addToData("two", "2".getBytes()) + .build(); + + V1SecretList secretList = new V1SecretList().addItemsItem(colorSecret).addItemsItem(shapeSecret); + + stubCall(secretList, "/api/v1/namespaces/default/secrets?labelSelector=color%3Dblue"); + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "blue"), false, + ConfigUtils.Prefix.DEFAULT); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).hasSize(1); + Assertions.assertThat(sourceData.sourceData().get("one")).isEqualTo("1"); + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.color-secret.default"); + + } + + /** + * two secrets are deployed: secret "color-secret" with label: "{color:blue}" and + * "color-secret-k8s" with label: "{color:blue}". We search by "{color:blue}" and find + * both. + */ + @Test + void searchWithLabelsOneSecretFoundAndOneFromProfileFound() { + + V1Secret colorSecret = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue")) + .withNamespace(NAMESPACE) + .withName("blue-ocean-secret") + .build()) + .addToData("one", "1".getBytes()) + .build(); + + V1Secret shapeSecret = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue")) + .withNamespace(NAMESPACE) + .withName("blue-sky-secret") + .build()) + .addToData("two", "2".getBytes()) + .build(); + + V1SecretList secretList = new V1SecretList().addItemsItem(colorSecret).addItemsItem(shapeSecret); + + stubCall(secretList, "/api/v1/namespaces/default/secrets?labelSelector=color%3Dblue"); + CoreV1Api api = new CoreV1Api(); + MockEnvironment environment = new MockEnvironment(); + + NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "blue"), false, + ConfigUtils.Prefix.DELAYED); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + false, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).hasSize(2); + Assertions.assertThat(sourceData.sourceData().get("blue-ocean-secret.one")).isEqualTo("1"); + Assertions.assertThat(sourceData.sourceData().get("blue-sky-secret.two")).isEqualTo("2"); + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.blue-ocean-secret.blue-sky-secret.default"); + + } + + /** + *
+	 *     - secret "color-secret" with label "{color:blue}"
+	 *     - secret "shape-secret" with labels "{color:blue, shape:round}"
+	 *     - secret "no-fit" with labels "{tag:no-fit}"
+	 *     - secret "color-secret-k8s" with label "{color:red}"
+	 *     - secret "shape-secret-k8s" with label "{shape:triangle}"
+	 * 
+ */ + @Test + void searchWithLabelsTwoSecretsFoundAndOneFromProfileFound() { + + V1Secret colorSecret = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue")) + .withNamespace(NAMESPACE) + .withName("color-secret") + .build()) + .addToData("one", "1".getBytes()) + .build(); + + V1Secret shapeSecret = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue", "shape", "round")) + .withNamespace(NAMESPACE) + .withName("shape-secret") + .build()) + .addToData("two", "2".getBytes()) + .build(); + + V1Secret noFit = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("tag", "no-fit")) + .withNamespace(NAMESPACE) + .withName("no-fit") + .build()) + .addToData("three", "3".getBytes()) + .build(); + + V1Secret colorSecretK8s = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue")) + .withNamespace(NAMESPACE) + .withName("color-secret-k8s") + .build()) + .addToData("four", "4".getBytes()) + .build(); + + V1Secret shapeSecretK8s = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue")) + .withNamespace(NAMESPACE) + .withName("shape-secret-k8s") + .build()) + .addToData("five", "5".getBytes()) + .build(); + + V1SecretList secretList = new V1SecretList().addItemsItem(colorSecret) + .addItemsItem(shapeSecret) + .addItemsItem(noFit) + .addItemsItem(colorSecretK8s) + .addItemsItem(shapeSecretK8s); + + stubCall(secretList, "/api/v1/namespaces/default/secrets?labelSelector=color%3Dblue"); + CoreV1Api api = new CoreV1Api(); + MockEnvironment environment = new MockEnvironment(); + + NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "blue"), false, + ConfigUtils.Prefix.DELAYED); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + false, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).hasSize(4); + Assertions.assertThat(sourceData.sourceData().get("color-secret.one")).isEqualTo("1"); + Assertions.assertThat(sourceData.sourceData().get("shape-secret.two")).isEqualTo("2"); + Assertions.assertThat(sourceData.sourceData().get("color-secret-k8s.four")).isEqualTo("4"); + Assertions.assertThat(sourceData.sourceData().get("shape-secret-k8s.five")).isEqualTo("5"); + + Assertions.assertThat(sourceData.sourceName()) + .isEqualTo("secret.color-secret.color-secret-k8s.shape-secret.shape-secret-k8s.default"); + + } + + /** + * yaml/properties gets special treatment + */ + @Test + void testYaml() { + V1Secret colorSecret = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withLabels(Map.of("color", "blue")) + .withNamespace(NAMESPACE) + .withName("color-secret") + .build()) + .addToData("test.yaml", "color: blue".getBytes()) + .build(); + + V1SecretList secretList = new V1SecretList().addItemsItem(colorSecret); + + stubCall(secretList, "/api/v1/namespaces/default/secrets?labelSelector=color%3Dblue"); + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "blue"), false, + ConfigUtils.Prefix.DEFAULT); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).hasSize(1); + Assertions.assertThat(sourceData.sourceData().get("color")).isEqualTo("blue"); + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.color-secret.default"); + } + + /** + *
+	 *     - one secret is deployed with label {"color", "red"}
+	 *     - one secret is deployed with label {"color", "green"}
+	 *
+	 *     - we first search for "red" and find it, and it is retrieved from the cluster via the client.
+	 * 	   - we then search for the "green" one, and it is not retrieved from the cache.
+	 * 
+ */ + @Test + void nonCache() { + 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, ReadType.SINGLE); + KubernetesClientContextToSourceData redData = new LabeledSecretContextToSourceDataProvider().get(); + SourceData redSourceData = redData.apply(redContext); + + Assertions.assertThat(redSourceData.sourceData()).hasSize(1); + Assertions.assertThat(redSourceData.sourceData().get("color")).isEqualTo("red"); + Assertions.assertThat(redSourceData.sourceName()).isEqualTo("secret.red.default"); + + NormalizedSource greenSource = new LabeledSecretNormalizedSource(NAMESPACE, Map.of("color", "green"), false, + ConfigUtils.Prefix.DEFAULT); + KubernetesClientConfigContext greenContext = new KubernetesClientConfigContext(api, greenSource, NAMESPACE, + new MockEnvironment(), false, ReadType.SINGLE); + KubernetesClientContextToSourceData greenData = new LabeledSecretContextToSourceDataProvider().get(); + SourceData greenSourceData = greenData.apply(greenContext); + + Assertions.assertThat(greenSourceData.sourceData()).hasSize(1); + Assertions.assertThat(greenSourceData.sourceData().get("color")).isEqualTo("green"); + Assertions.assertThat(greenSourceData.sourceName()).isEqualTo("secret.green.default"); + + // no caching + verify(0, getRequestedFor(urlEqualTo("/api/v1/namespaces/default/secrets"))); + verify(1, getRequestedFor(urlEqualTo("/api/v1/namespaces/default/secrets?labelSelector=color%3Dred"))); + verify(1, getRequestedFor(urlEqualTo("/api/v1/namespaces/default/secrets?labelSelector=color%3Dgreen"))); + } + + private void stubCall(V1SecretList configMapList, String path) { + stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(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/NamedConfigMapContextToSourceDataProviderBatchReadTests.java similarity index 92% 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/NamedConfigMapContextToSourceDataProviderBatchReadTests.java index cd0acab2c0..95aadcbcff 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/NamedConfigMapContextToSourceDataProviderBatchReadTests.java @@ -31,29 +31,30 @@ import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; import io.kubernetes.client.util.ClientBuilder; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.test.system.CapturedOutput; -import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.config.ConfigUtils; import org.springframework.cloud.kubernetes.commons.config.NamedConfigMapNormalizedSource; import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.ReadType; 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.getRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; /** * @author wind57 */ -@ExtendWith(OutputCaptureExtension.class) -class NamedConfigMapContextToSourceDataProviderTests { +class NamedConfigMapContextToSourceDataProviderBatchReadTests { private static final String NAMESPACE = "default"; @@ -82,7 +83,12 @@ static void setup() { @AfterEach void afterEach() { WireMock.reset(); - new KubernetesClientConfigMapsCache().discardAll(); + KubernetesClientSourcesBatchRead.discardConfigMaps(); + } + + @AfterAll + static void afterAll() { + WireMock.shutdownServer(); } /** @@ -104,7 +110,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, ReadType.BATCH); KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -134,7 +140,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, ReadType.BATCH); KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -147,8 +153,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 +177,7 @@ void matchIncludeSingleProfile() { MockEnvironment environment = new MockEnvironment(); environment.addActiveProfile("with-profile"); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, - true); + true, ReadType.BATCH); KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -215,7 +219,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, ReadType.BATCH); KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -270,7 +275,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, ReadType.BATCH); KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -304,7 +310,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, ReadType.BATCH); KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -335,7 +341,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, ReadType.BATCH); KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -362,7 +368,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, ReadType.BATCH); KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -394,7 +400,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, ReadType.BATCH); KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -413,7 +420,7 @@ void testCorrectNameWithProfile() { * */ @Test - void cache(CapturedOutput output) { + void cache() { V1ConfigMap red = new V1ConfigMapBuilder() .withMetadata(new V1ObjectMetaBuilder().withName("red").withNamespace(NAMESPACE).build()) .addToData("color", "red") @@ -433,36 +440,29 @@ void cache(CapturedOutput output) { NormalizedSource redSource = new NamedConfigMapNormalizedSource("red", NAMESPACE, true, false); KubernetesClientConfigContext redContext = new KubernetesClientConfigContext(api, redSource, NAMESPACE, - environment); + environment, true, ReadType.BATCH); KubernetesClientContextToSourceData redData = new NamedConfigMapContextToSourceDataProvider().get(); SourceData redSourceData = redData.apply(redContext); Assertions.assertThat(redSourceData.sourceName()).isEqualTo("configmap.red.default"); Assertions.assertThat(redSourceData.sourceData()).isEqualTo(Map.of("color", "red")); - Assertions.assertThat(output.getOut()).contains("Loaded all config maps in namespace '" + NAMESPACE + "'"); NormalizedSource greenSource = new NamedConfigMapNormalizedSource("green", NAMESPACE, true, true); KubernetesClientConfigContext greenContext = new KubernetesClientConfigContext(api, greenSource, NAMESPACE, - environment); + environment, false, ReadType.BATCH); KubernetesClientContextToSourceData greenData = new NamedConfigMapContextToSourceDataProvider().get(); SourceData greenSourceData = greenData.apply(greenContext); Assertions.assertThat(greenSourceData.sourceName()).isEqualTo("configmap.green.default"); Assertions.assertThat(greenSourceData.sourceData()).isEqualTo(Map.of("color", "green")); - // meaning there is a single entry with such a log statement - String[] out = output.getAll().split("Loaded all config maps in namespace"); - Assertions.assertThat(out.length).isEqualTo(2); - - // meaning that the second read was done from the cache - out = output.getAll().split("Loaded \\(from cache\\) all config maps in namespace"); - Assertions.assertThat(out.length).isEqualTo(2); - + // called only once, since the use caching + verify(1, getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps"))); } private void stubCall(V1ConfigMapList list) { stubFor(get("/api/v1/namespaces/default/configmaps") - .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(list)))); + .willReturn(aResponse().withStatus(200).withBody(JSON.serialize(list)))); } } diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/NamedConfigMapContextToSourceDataProviderSingleReadTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/NamedConfigMapContextToSourceDataProviderSingleReadTests.java new file mode 100644 index 0000000000..9c2ada2eab --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/NamedConfigMapContextToSourceDataProviderSingleReadTests.java @@ -0,0 +1,458 @@ +/* + * Copyright 2013-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client.config; + +import java.util.Map; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.Configuration; +import io.kubernetes.client.openapi.JSON; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1ConfigMap; +import io.kubernetes.client.openapi.models.V1ConfigMapBuilder; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.util.ClientBuilder; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.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.ReadType; +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.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; + +/** + * @author wind57 + */ +class NamedConfigMapContextToSourceDataProviderSingleReadTests { + + 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(); + KubernetesClientSourcesBatchRead.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, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.blue.default"); + Assertions.assertThat(sourceData.sourceData()).isEmpty(); + + } + + /** + *
+	 *     one configmap deployed with name "red"
+	 *     we search by name, for the "red" one, as such we find it
+	 * 
+ */ + @Test + void match() { + + V1ConfigMap configMap = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME).withNamespace(NAMESPACE).build()) + .addToData(COLOR_REALLY_RED) + .build(); + stubCall(configMap, "/api/v1/namespaces/default/configmaps/red"); + + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new NamedConfigMapNormalizedSource(RED_CONFIG_MAP_NAME, NAMESPACE, true, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), true, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red.default"); + Assertions.assertThat(sourceData.sourceData()).isEqualTo(COLOR_REALLY_RED); + + } + + /** + *
+	 *     - two configmaps deployed : "red" and "red-with-profile".
+	 * 
+ */ + @Test + void matchIncludeSingleProfile() { + + V1ConfigMap red = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME).withNamespace(NAMESPACE).build()) + .addToData(COLOR_REALLY_RED) + .build(); + stubCall(red, "/api/v1/namespaces/default/configmaps/red"); + + V1ConfigMap redWithProfile = new V1ConfigMapBuilder().withMetadata( + new V1ObjectMetaBuilder().withName(RED_WITH_PROFILE_CONFIG_MAP_NAME).withNamespace(NAMESPACE).build()) + .addToData(TASTE_MANGO) + .build(); + stubCall(redWithProfile, "/api/v1/namespaces/default/configmaps/red-with-profile"); + + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new NamedConfigMapNormalizedSource(RED_CONFIG_MAP_NAME, NAMESPACE, true, + ConfigUtils.Prefix.DEFAULT, true, true); + MockEnvironment environment = new MockEnvironment(); + environment.setActiveProfiles("with-profile"); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + true, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red.red-with-profile.default.with-profile"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("color", "really-red", "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, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red.red-with-profile.default"); + Assertions.assertThat(sourceData.sourceData()).hasSize(2); + Assertions.assertThat(sourceData.sourceData().get("some.color")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData().get("some.taste")).isEqualTo("mango"); + + } + + /** + *
+	 *     - three configmaps deployed : "red", "red-with-taste" and "red-with-shape"
+	 *     - "red" is matched directly, the other two are matched because of active profiles
+	 *     -  This takes into consideration the prefix, that we explicitly specify.
+	 *        Notice that prefix works for profile based config maps as well.
+	 * 
+ */ + @Test + void matchIncludeTwoProfilesWithPrefix() { + + V1ConfigMap red = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME).withNamespace(NAMESPACE).build()) + .addToData(COLOR_REALLY_RED) + .build(); + stubCall(red, "/api/v1/namespaces/default/configmaps/red"); + + V1ConfigMap redWithTaste = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME + "-with-taste") + .withNamespace(NAMESPACE) + .withResourceVersion("1") + .build()) + .addToData(TASTE_MANGO) + .build(); + stubCall(redWithTaste, "/api/v1/namespaces/default/configmaps/red-with-taste"); + + V1ConfigMap redWithShape = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME + "-with-shape") + .withNamespace(NAMESPACE) + .build()) + .addToData("shape", "round") + .build(); + stubCall(redWithShape, "/api/v1/namespaces/default/configmaps/red-with-shape"); + + CoreV1Api api = new CoreV1Api(); + + ConfigUtils.Prefix prefix = ConfigUtils.findPrefix("some", false, false, null); + NormalizedSource source = new NamedConfigMapNormalizedSource(RED_CONFIG_MAP_NAME, NAMESPACE, true, prefix, + true); + MockEnvironment environment = new MockEnvironment(); + environment.setActiveProfiles("with-taste", "with-shape"); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + true, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red.red-with-shape.red-with-taste.default"); + Assertions.assertThat(sourceData.sourceData()).hasSize(3); + Assertions.assertThat(sourceData.sourceData().get("some.color")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData().get("some.taste")).isEqualTo("mango"); + Assertions.assertThat(sourceData.sourceData().get("some.shape")).isEqualTo("round"); + + } + + /** + *
+	 * 		proves that an implicit configmap is not going to be generated and read
+	 * 
+ */ + @Test + void matchWithName() { + + V1ConfigMap red = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("application").withNamespace(NAMESPACE).build()) + .addToData("color", "red") + .build(); + stubCall(red, "/api/v1/namespaces/default/configmaps/red"); + + CoreV1Api api = new CoreV1Api(); + + ConfigUtils.Prefix prefix = ConfigUtils.findPrefix("some", false, false, null); + NormalizedSource source = new NamedConfigMapNormalizedSource("application", NAMESPACE, true, prefix, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), true, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.application.default"); + Assertions.assertThat(sourceData.sourceData()).isEmpty(); + } + + /** + *
+	 *     - NamedSecretContextToSourceDataProvider gets as input a KubernetesClientConfigContext.
+	 *     - This context has a namespace as well as a NormalizedSource, that has a namespace too.
+	 *     - This test makes sure that we use the proper one.
+	 * 
+ */ + @Test + void namespaceMatch() { + + V1ConfigMap configMap = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME).withNamespace(NAMESPACE).build()) + .addToData(COLOR_REALLY_RED) + .build(); + stubCall(configMap, "/api/v1/namespaces/default/configmaps/red"); + + CoreV1Api api = new CoreV1Api(); + + String wrongNamespace = NAMESPACE + "nope"; + NormalizedSource source = new NamedConfigMapNormalizedSource(RED_CONFIG_MAP_NAME, wrongNamespace, true, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), true, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red.default"); + Assertions.assertThat(sourceData.sourceData()).isEqualTo(COLOR_REALLY_RED); + } + + /** + *
+	 *     - proves that single yaml file gets special treatment
+	 * 
+ */ + @Test + void testSingleYaml() { + V1ConfigMap singleYaml = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName(RED_CONFIG_MAP_NAME).withNamespace(NAMESPACE).build()) + .addToData("single.yaml", "key: value") + .build(); + stubCall(singleYaml, "/api/v1/namespaces/default/configmaps/red"); + + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new NamedConfigMapNormalizedSource(RED_CONFIG_MAP_NAME, NAMESPACE, true, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), true, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red.default"); + Assertions.assertThat(sourceData.sourceData()).containsExactlyInAnyOrderEntriesOf(Map.of("key", "value")); + } + + /** + *
+	 *     - one configmap is deployed with name "one"
+	 *     - profile is enabled with name "k8s"
+	 *
+	 *     we assert that the name of the source is "one" and does not contain "one-dev"
+	 * 
+ */ + @Test + void testCorrectNameWithProfile() { + V1ConfigMap one = new V1ConfigMapBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("one").withNamespace(NAMESPACE).build()) + .addToData("key", "value") + .build(); + stubCall(one, "/api/v1/namespaces/default/configmaps/one"); + + CoreV1Api api = new CoreV1Api(); + + MockEnvironment environment = new MockEnvironment(); + environment.setActiveProfiles("k8s"); + + NormalizedSource source = new NamedConfigMapNormalizedSource("one", NAMESPACE, true, true); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + true, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.one.default"); + Assertions.assertThat(sourceData.sourceData()).containsExactlyInAnyOrderEntriesOf(Map.of("key", "value")); + } + + /** + *
+	 *     - one configmap is deployed with name "red"
+	 *     - one configmap is deployed with name "green"
+	 *
+	 *     - we first search for "red" and find it, and it is retrieved from the cluster via the client.
+	 * 	   - we then search for the "green" one, and it is retrieved again from the cluster, non cached.
+	 * 
+ */ + @Test + void nonCache() { + 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, ReadType.SINGLE); + KubernetesClientContextToSourceData redData = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData redSourceData = redData.apply(redContext); + + Assertions.assertThat(redSourceData.sourceName()).isEqualTo("configmap.red.default"); + Assertions.assertThat(redSourceData.sourceData()).containsExactlyInAnyOrderEntriesOf(Map.of("color", "red")); + + NormalizedSource greenSource = new NamedConfigMapNormalizedSource("green", NAMESPACE, true, true); + KubernetesClientConfigContext greenContext = new KubernetesClientConfigContext(api, greenSource, NAMESPACE, + environment, false, ReadType.SINGLE); + KubernetesClientContextToSourceData greenData = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData greenSourceData = greenData.apply(greenContext); + + Assertions.assertThat(greenSourceData.sourceName()).isEqualTo("configmap.green.default"); + Assertions.assertThat(greenSourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("color", "green")); + + // no caching + verify(0, getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps"))); + verify(1, getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps/red"))); + verify(1, getRequestedFor(urlEqualTo("/api/v1/namespaces/default/configmaps/green"))); + } + + private void stubCall(V1ConfigMap configMap, String path) { + stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(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/NamedSecretContextToSourceDataProviderBatchReadTests.java similarity index 91% 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/NamedSecretContextToSourceDataProviderBatchReadTests.java index 0d54212f73..176c0108d8 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/NamedSecretContextToSourceDataProviderBatchReadTests.java @@ -31,26 +31,27 @@ import io.kubernetes.client.openapi.models.V1SecretListBuilder; import io.kubernetes.client.util.ClientBuilder; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -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.ReadType; 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.getRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; -@ExtendWith(OutputCaptureExtension.class) -class NamedSecretContextToSourceDataProviderTests { +class NamedSecretContextToSourceDataProviderBatchReadTests { private static final ConfigUtils.Prefix PREFIX = ConfigUtils.findPrefix("some", false, false, "irrelevant"); @@ -73,7 +74,12 @@ static void setup() { @AfterEach void afterEach() { WireMock.reset(); - new KubernetesClientSecretsCache().discardAll(); + KubernetesClientSourcesBatchRead.discardSecrets(); + } + + @AfterAll + static void afterAll() { + WireMock.shutdownServer(); } /** @@ -93,7 +99,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, ReadType.BATCH); KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -133,7 +139,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, ReadType.BATCH); KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -162,7 +168,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, ReadType.BATCH); KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -193,7 +199,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, ReadType.BATCH); KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -229,15 +235,14 @@ void matchIncludeSingleProfile() { MockEnvironment environment = new MockEnvironment(); environment.addActiveProfile("with-profile"); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, - true); + true, ReadType.BATCH); KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.red-with-profile.default.with-profile"); - Assertions.assertThat(sourceData.sourceData().size()).isEqualTo(2); - Assertions.assertThat(sourceData.sourceData().get("taste")).isEqualTo("mango"); - Assertions.assertThat(sourceData.sourceData().get("color")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("color", "really-red", "taste", "mango")); } @@ -268,7 +273,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, ReadType.BATCH); KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -312,7 +318,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, ReadType.BATCH); KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -344,7 +351,7 @@ void testSingleYaml() { NormalizedSource source = new NamedSecretNormalizedSource("single-yaml", NAMESPACE, true, false); KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), false, ReadType.BATCH); KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -363,7 +370,7 @@ void testSingleYaml() { * */ @Test - void cache(CapturedOutput output) { + void cache() { V1Secret red = new V1SecretBuilder() .withMetadata(new V1ObjectMetaBuilder().withName("red").withNamespace(NAMESPACE).build()) .addToData("color", "red".getBytes()) @@ -383,36 +390,29 @@ void cache(CapturedOutput output) { NormalizedSource redSource = new NamedSecretNormalizedSource("red", NAMESPACE, true, false); KubernetesClientConfigContext redContext = new KubernetesClientConfigContext(api, redSource, NAMESPACE, - environment); + environment, false, ReadType.BATCH); KubernetesClientContextToSourceData redData = new NamedSecretContextToSourceDataProvider().get(); SourceData redSourceData = redData.apply(redContext); Assertions.assertThat(redSourceData.sourceName()).isEqualTo("secret.red.default"); Assertions.assertThat(redSourceData.sourceData()).isEqualTo(Map.of("color", "red")); - Assertions.assertThat(output.getAll()).contains("Loaded all secrets in namespace '" + NAMESPACE + "'"); NormalizedSource greenSource = new NamedSecretNormalizedSource("green", NAMESPACE, true, true); KubernetesClientConfigContext greenContext = new KubernetesClientConfigContext(api, greenSource, NAMESPACE, - environment); + environment, false, ReadType.BATCH); KubernetesClientContextToSourceData greenData = new NamedSecretContextToSourceDataProvider().get(); SourceData greenSourceData = greenData.apply(greenContext); Assertions.assertThat(greenSourceData.sourceName()).isEqualTo("secret.green.default"); Assertions.assertThat(greenSourceData.sourceData()).isEqualTo(Map.of("color", "green")); - // meaning there is a single entry with such a log statement - String[] out = output.getAll().split("Loaded all secrets in namespace"); - Assertions.assertThat(out.length).isEqualTo(2); - - // meaning that the second read was done from the cache - out = output.getAll().split("Loaded \\(from cache\\) all secrets in namespace"); - Assertions.assertThat(out.length).isEqualTo(2); - + // called only once, since the use caching + verify(1, getRequestedFor(urlEqualTo("/api/v1/namespaces/default/secrets"))); } private void stubCall(V1SecretList list) { stubFor(get("/api/v1/namespaces/default/secrets") - .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(list)))); + .willReturn(aResponse().withStatus(200).withBody(JSON.serialize(list)))); } } diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/NamedSecretContextToSourceDataProviderSingleReadTests.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/NamedSecretContextToSourceDataProviderSingleReadTests.java new file mode 100644 index 0000000000..0656ec56fe --- /dev/null +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/NamedSecretContextToSourceDataProviderSingleReadTests.java @@ -0,0 +1,414 @@ +/* + * Copyright 2013-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client.config; + +import java.util.Map; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.Configuration; +import io.kubernetes.client.openapi.JSON; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.openapi.models.V1Secret; +import io.kubernetes.client.openapi.models.V1SecretBuilder; +import io.kubernetes.client.util.ClientBuilder; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.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.ReadType; +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.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; + +class NamedSecretContextToSourceDataProviderSingleReadTests { + + 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(); + KubernetesClientSourcesBatchRead.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, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("color", "really-red")); + + } + + /** + * we have three secrets deployed. one of them has a name that matches (red), the + * other two have different names, thus no match. + */ + @Test + void twoSecretMatchAgainstLabels() { + + V1Secret red = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("red").build()) + .addToData(COLOR_REALLY_RED) + .build(); + stubCall(red, "/api/v1/namespaces/default/secrets/red"); + + V1Secret blue = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("blue").build()) + .addToData(COLOR_REALLY_RED) + .build(); + stubCall(blue, "/api/v1/namespaces/default/secrets/blue"); + + V1Secret pink = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("pink").build()) + .addToData(COLOR_REALLY_RED) + .build(); + stubCall(pink, "/api/v1/namespaces/default/secrets/pink"); + + CoreV1Api api = new CoreV1Api(); + + // blue does not match red, nor pink + NormalizedSource source = new NamedSecretNormalizedSource("red", NAMESPACE, false, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.default"); + Assertions.assertThat(sourceData.sourceData()).hasSize(1); + Assertions.assertThat(sourceData.sourceData().get("color")).isEqualTo("really-red"); + + } + + /** + * one secret deployed (pink), does not match our query (blue). + */ + @Test + void testSecretNoMatch() { + + V1Secret secret = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("red").build()) + .addToData(COLOR_REALLY_RED) + .build(); + + stubCall(secret, "/api/v1/namespaces/default/secrets/blue"); + CoreV1Api api = new CoreV1Api(); + + // blue does not match red + NormalizedSource source = new NamedSecretNormalizedSource("blue", NAMESPACE, false, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.blue.default"); + Assertions.assertThat(sourceData.sourceData()).isEmpty(); + } + + /** + *
+	 *     - LabeledSecretContextToSourceDataProvider gets as input a KubernetesClientConfigContext.
+	 *     - This context has a namespace as well as a NormalizedSource, that has a namespace too.
+	 *     - This test makes sure that we use the proper one.
+	 * 
+ */ + @Test + void namespaceMatch() { + + V1Secret secret = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("red").build()) + .addToData(COLOR_REALLY_RED) + .build(); + + stubCall(secret, "/api/v1/namespaces/default/secrets/red"); + CoreV1Api api = new CoreV1Api(); + + String wrongNamespace = NAMESPACE + "nope"; + NormalizedSource source = new NamedSecretNormalizedSource("red", wrongNamespace, false, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("color", "really-red")); + } + + /** + * we have two secrets deployed. one matches the query name. the other matches the + * active profile + name, thus is taken also. + */ + @Test + void matchIncludeSingleProfile() { + + V1Secret red = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("red").build()) + .addToData(COLOR_REALLY_RED) + .build(); + stubCall(red, "/api/v1/namespaces/default/secrets/red"); + + V1Secret mango = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("red-with-profile").build()) + .addToData("taste", "mango".getBytes()) + .build(); + stubCall(mango, "/api/v1/namespaces/default/secrets/red-with-profile"); + + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new NamedSecretNormalizedSource("red", NAMESPACE, false, ConfigUtils.Prefix.DEFAULT, + true, true); + MockEnvironment environment = new MockEnvironment(); + environment.addActiveProfile("with-profile"); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + true, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.red-with-profile.default.with-profile"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("color", "really-red", "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, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.red-with-taste.default"); + Assertions.assertThat(sourceData.sourceData()).hasSize(2); + Assertions.assertThat(sourceData.sourceData().get("some.color")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData().get("some.taste")).isEqualTo("mango"); + + } + + /** + * we have three secrets deployed. one matches the query name. the other two match the + * active profile + name, thus are taken also. This takes into consideration the + * prefix, that we explicitly specify. Notice that prefix works for profile based + * config maps as well. + */ + @Test + void matchIncludeTwoProfilesWithPrefix() { + + V1Secret red = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("red").build()) + .addToData(COLOR_REALLY_RED) + .build(); + stubCall(red, "/api/v1/namespaces/default/secrets/red"); + + V1Secret mango = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("red-with-taste").build()) + .addToData("taste", "mango".getBytes()) + .build(); + stubCall(mango, "/api/v1/namespaces/default/secrets/red-with-taste"); + + V1Secret shape = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withNamespace(NAMESPACE).withName("red-with-shape").build()) + .addToData("shape", "round".getBytes()) + .build(); + stubCall(shape, "/api/v1/namespaces/default/secrets/red-with-shape"); + + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new NamedSecretNormalizedSource("red", NAMESPACE, true, PREFIX, true); + MockEnvironment environment = new MockEnvironment(); + environment.setActiveProfiles("with-taste", "with-shape"); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, environment, + true, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.red-with-shape.red-with-taste.default"); + + Assertions.assertThat(sourceData.sourceData()).hasSize(3); + Assertions.assertThat(sourceData.sourceData().get("some.color")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData().get("some.taste")).isEqualTo("mango"); + Assertions.assertThat(sourceData.sourceData().get("some.shape")).isEqualTo("round"); + + } + + /** + *
+	 *     - proves that single yaml file gets special treatment
+	 * 
+ */ + @Test + void testSingleYaml() { + V1Secret singleYaml = new V1SecretBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("single-yaml").withNamespace(NAMESPACE).build()) + .addToData("single.yaml", "key: value".getBytes()) + .build(); + stubCall(singleYaml, "/api/v1/namespaces/default/secrets/single-yaml"); + + CoreV1Api api = new CoreV1Api(); + + NormalizedSource source = new NamedSecretNormalizedSource("single-yaml", NAMESPACE, true, false); + KubernetesClientConfigContext context = new KubernetesClientConfigContext(api, source, NAMESPACE, + new MockEnvironment(), false, ReadType.SINGLE); + + KubernetesClientContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.single-yaml.default"); + Assertions.assertThat(sourceData.sourceData()).containsExactlyInAnyOrderEntriesOf(Map.of("key", "value")); + } + + /** + *
+	 *     - one secret is deployed with name "red"
+	 *     - one secret is deployed with name "green"
+	 *
+	 *     - we first search for "red" and find it, and it is retrieved from the cluster via the client.
+	 * 	   - we then search for the "green" one, and it is retrieved again from the cluster, non cached.
+	 * 
+ */ + @Test + void nonCache() { + 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, ReadType.SINGLE); + KubernetesClientContextToSourceData redData = new NamedSecretContextToSourceDataProvider().get(); + SourceData redSourceData = redData.apply(redContext); + + Assertions.assertThat(redSourceData.sourceName()).isEqualTo("secret.red.default"); + Assertions.assertThat(redSourceData.sourceData()).containsExactlyInAnyOrderEntriesOf(Map.of("color", "red")); + + NormalizedSource greenSource = new NamedSecretNormalizedSource("green", NAMESPACE, true, true); + KubernetesClientConfigContext greenContext = new KubernetesClientConfigContext(api, greenSource, NAMESPACE, + environment, false, ReadType.SINGLE); + KubernetesClientContextToSourceData greenData = new NamedSecretContextToSourceDataProvider().get(); + SourceData greenSourceData = greenData.apply(greenContext); + + Assertions.assertThat(greenSourceData.sourceName()).isEqualTo("secret.green.default"); + Assertions.assertThat(greenSourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("color", "green")); + + // no caching + verify(0, getRequestedFor(urlEqualTo("/api/v1/namespaces/default/secrets"))); + verify(1, getRequestedFor(urlEqualTo("/api/v1/namespaces/default/secrets/red"))); + verify(1, getRequestedFor(urlEqualTo("/api/v1/namespaces/default/secrets/green"))); + + } + + private void stubCall(V1Secret secret, String path) { + stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(JSON.serialize(secret)))); + } + +} 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 2f9bb0f851..54d4119af7 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,16 @@ package org.springframework.cloud.kubernetes.client.config.applications.sources_order; +import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +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; @@ -50,6 +56,18 @@ void afterEach() { WireMock.reset(); } + @BeforeAll + static void setup() { + WireMockServer wireMockServer = new WireMockServer(WireMockConfiguration.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); + } + @AfterAll static void afterAll() { WireMock.shutdownServer(); diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadConfigMapTest.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadConfigMapTest.java index 61589a7e20..e44925efda 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadConfigMapTest.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadConfigMapTest.java @@ -51,6 +51,7 @@ import org.springframework.cloud.kubernetes.client.config.VisibleKubernetesClientEventBasedConfigMapChangeDetector; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.cloud.kubernetes.commons.config.RetryProperties; import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy; @@ -217,7 +218,7 @@ AbstractEnvironment environment() { // otherwise we can't properly test reload functionality ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, false, true, FAIL_FAST, - RetryProperties.DEFAULT); + RetryProperties.DEFAULT, ReadType.BATCH); KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); PropertySource propertySource = new KubernetesClientConfigMapPropertySourceLocator(coreV1Api, @@ -240,7 +241,7 @@ ConfigReloadProperties configReloadProperties() { @Primary ConfigMapConfigProperties configMapConfigProperties() { return new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, - false, true, FAIL_FAST, RetryProperties.DEFAULT); + false, true, FAIL_FAST, RetryProperties.DEFAULT, ReadType.BATCH); } @Bean diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadSecretTest.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadSecretTest.java index 2a38916251..9aa33e46fc 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadSecretTest.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/EventReloadSecretTest.java @@ -52,6 +52,7 @@ import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsPropertySourceLocator; import org.springframework.cloud.kubernetes.client.config.VisibleKubernetesClientEventBasedSecretsChangeDetector; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.cloud.kubernetes.commons.config.RetryProperties; import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties; import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; @@ -221,7 +222,8 @@ AbstractEnvironment environment() { // KubernetesClientConfigMapPropertySource, // otherwise we can't properly test reload functionality SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), - List.of(), true, SECRET_NAME, NAMESPACE, false, true, FAIL_FAST, RetryProperties.DEFAULT); + List.of(), true, SECRET_NAME, NAMESPACE, false, true, FAIL_FAST, RetryProperties.DEFAULT, + ReadType.BATCH); KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); PropertySource propertySource = new KubernetesClientSecretsPropertySourceLocator(coreV1Api, @@ -244,7 +246,7 @@ ConfigReloadProperties configReloadProperties() { @Primary SecretsConfigProperties secretsConfigProperties() { return new SecretsConfigProperties(true, Map.of(), List.of(), List.of(), true, SECRET_NAME, NAMESPACE, - false, true, FAIL_FAST, RetryProperties.DEFAULT); + false, true, FAIL_FAST, RetryProperties.DEFAULT, ReadType.BATCH); } @Bean diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadConfigMapTest.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadConfigMapTest.java index aa1919e8bf..6ced5a499b 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadConfigMapTest.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadConfigMapTest.java @@ -47,6 +47,7 @@ import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySourceLocator; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.cloud.kubernetes.commons.config.RetryProperties; import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy; @@ -192,7 +193,8 @@ AbstractEnvironment environment() { // KubernetesClientConfigMapPropertySource, // otherwise we can't properly test reload functionality ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), - List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT); + List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT, + ReadType.BATCH); KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); PropertySource propertySource = new KubernetesClientConfigMapPropertySourceLocator(coreV1Api, @@ -215,7 +217,7 @@ ConfigReloadProperties configReloadProperties() { @Primary ConfigMapConfigProperties configMapConfigProperties() { return new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, - false, true, FAIL_FAST, RetryProperties.DEFAULT); + false, true, FAIL_FAST, RetryProperties.DEFAULT, ReadType.BATCH); } @Bean diff --git a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadSecretTest.java b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadSecretTest.java index 74756a169d..641eb717bb 100644 --- a/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadSecretTest.java +++ b/spring-cloud-kubernetes-client-config/src/test/java/org/springframework/cloud/kubernetes/client/config/reload_it/PollingReloadSecretTest.java @@ -48,6 +48,7 @@ import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsPropertySource; import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsPropertySourceLocator; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.cloud.kubernetes.commons.config.RetryProperties; import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties; import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; @@ -194,7 +195,8 @@ AbstractEnvironment environment() { // KubernetesClientSecretPropertySource, // otherwise we can't properly test reload functionality SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), - List.of(), true, SECRET_NAME, NAMESPACE, false, true, false, RetryProperties.DEFAULT); + List.of(), true, SECRET_NAME, NAMESPACE, false, true, false, RetryProperties.DEFAULT, + ReadType.BATCH); KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); PropertySource propertySource = new KubernetesClientSecretsPropertySourceLocator(coreV1Api, @@ -217,7 +219,7 @@ ConfigReloadProperties configReloadProperties() { @Primary SecretsConfigProperties secretsConfigProperties() { return new SecretsConfigProperties(true, Map.of(), List.of(), List.of(), true, SECRET_NAME, NAMESPACE, - false, true, FAIL_FAST, RetryProperties.DEFAULT); + false, true, FAIL_FAST, RetryProperties.DEFAULT, ReadType.BATCH); } @Bean diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigDataRetryableConfigMapPropertySourceLocator.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigDataRetryableConfigMapPropertySourceLocator.java index 233b3cc42d..288636c301 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 @@ -36,9 +36,8 @@ public class ConfigDataRetryableConfigMapPropertySourceLocator extends ConfigMap private ConfigMapPropertySourceLocator configMapPropertySourceLocator; public ConfigDataRetryableConfigMapPropertySourceLocator( - ConfigMapPropertySourceLocator configMapPropertySourceLocator, ConfigMapConfigProperties properties, - ConfigMapCache cache) { - super(properties, cache); + ConfigMapPropertySourceLocator configMapPropertySourceLocator, ConfigMapConfigProperties properties) { + super(properties); this.configMapPropertySourceLocator = configMapPropertySourceLocator; this.retryTemplate = RetryTemplate.builder() .maxAttempts(properties.retry().maxAttempts()) @@ -49,8 +48,8 @@ public ConfigDataRetryableConfigMapPropertySourceLocator( @Override protected MapPropertySource getMapPropertySource(NormalizedSource normalizedSource, - ConfigurableEnvironment environment) { - return configMapPropertySourceLocator.getMapPropertySource(normalizedSource, environment); + ConfigurableEnvironment environment, ReadType readType) { + return configMapPropertySourceLocator.getMapPropertySource(normalizedSource, environment, readType); } @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 a0e62cc795..06227f4b17 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 @@ -35,8 +35,8 @@ public class ConfigDataRetryableSecretsPropertySourceLocator extends SecretsProp private SecretsPropertySourceLocator secretsPropertySourceLocator; public ConfigDataRetryableSecretsPropertySourceLocator(SecretsPropertySourceLocator propertySourceLocator, - SecretsConfigProperties secretsConfigProperties, SecretsCache cache) { - super(secretsConfigProperties, cache); + SecretsConfigProperties secretsConfigProperties) { + super(secretsConfigProperties); this.secretsPropertySourceLocator = propertySourceLocator; this.retryTemplate = RetryTemplate.builder() .maxAttempts(properties.retry().maxAttempts()) @@ -57,8 +57,8 @@ public Collection> locateCollection(Environment environment) { @Override protected SecretsPropertySource getPropertySource(ConfigurableEnvironment environment, - NormalizedSource normalizedSource) { - return this.secretsPropertySourceLocator.getPropertySource(environment, normalizedSource); + NormalizedSource normalizedSource, ReadType readType) { + return this.secretsPropertySourceLocator.getPropertySource(environment, normalizedSource, readType); } 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 deleted file mode 100644 index 5d70e66f6a..0000000000 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapCache.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2013-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.commons.config; - -/** - * @author wind57 - */ -public interface ConfigMapCache { - - /** - * Discards all stored entries from the cache. - */ - void discardAll(); - - /** - * an implementation that does nothing. In the next major release it will become - * absolute and must be removed. - */ - class NOOPCache implements ConfigMapCache { - - @Override - public void discardAll() { - } - - } - -} 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 661b01c271..e082ad0c63 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("BATCH") ReadType readType) { /** * 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 6f03effae2..dd5710fd20 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 @@ -52,17 +52,14 @@ public abstract class ConfigMapPropertySourceLocator implements PropertySourceLo private static final Log LOG = LogFactory.getLog(ConfigMapPropertySourceLocator.class); - private final ConfigMapCache cache; - protected final ConfigMapConfigProperties properties; - public ConfigMapPropertySourceLocator(ConfigMapConfigProperties properties, ConfigMapCache cache) { + public ConfigMapPropertySourceLocator(ConfigMapConfigProperties properties) { this.properties = properties; - this.cache = cache; } protected abstract MapPropertySource getMapPropertySource(NormalizedSource normalizedSource, - ConfigurableEnvironment environment); + ConfigurableEnvironment environment, ReadType readType); @Override public PropertySource locate(Environment environment) { @@ -72,10 +69,11 @@ 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 -> { - MapPropertySource propertySource = getMapPropertySource(s, env); + sources.forEach(configMapSource -> { + MapPropertySource propertySource = getMapPropertySource(configMapSource, env, + properties.readType()); if ("true".equals(propertySource.getProperty(Constants.ERROR_PROPERTY))) { - LOG.warn("Failed to load source: " + s); + LOG.warn("Failed to load source: " + configMapSource); } else { LOG.debug("Adding config map property source " + propertySource.getName()); @@ -86,7 +84,6 @@ public PropertySource locate(Environment environment) { addPropertySourcesFromPaths(environment, composite); - cache.discardAll(); return composite; } return null; 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 0b5bdd130e..51d4930ace 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 @@ -103,6 +103,6 @@ private SourceData emptySourceData(Map labels, String target, St * @return a container that holds the names of the source that were found and their * data */ - public abstract MultipleSourcesContainer dataSupplier(Map labels); + protected 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 28d28e1b2e..075ee462a3 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 @@ -121,6 +121,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/ReadType.java similarity index 70% rename from spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/SecretsCache.java rename to spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/ReadType.java index 1bf399fcbd..c0c9ca0f12 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/ReadType.java @@ -19,23 +19,15 @@ /** * @author wind57 */ -public interface SecretsCache { +public enum ReadType { /** - * Discards all stored entries from the cache. + * read sources in a batch (all in namespace). */ - void discardAll(); - + BATCH, /** - * an implementation that does nothing. In the next major release it will become - * absolute and must be removed. + * read sources individually. */ - class NOOPCache implements SecretsCache { - - @Override - public void discardAll() { - } - - } + SINGLE; } 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 a65a20a276..8ad636d805 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("BATCH") ReadType readType) { /** * Prefix for Kubernetes secrets configuration properties. 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 53414b39be..9602d8b8d6 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 @@ -58,13 +58,10 @@ public abstract class SecretsPropertySourceLocator implements PropertySourceLoca private static final Log LOG = LogFactory.getLog(SecretsPropertySourceLocator.class); - private final SecretsCache cache; - protected final SecretsConfigProperties properties; - public SecretsPropertySourceLocator(SecretsConfigProperties properties, SecretsCache cache) { + public SecretsPropertySourceLocator(SecretsConfigProperties properties) { this.properties = properties; - this.cache = cache; } @Override @@ -79,11 +76,11 @@ public PropertySource locate(Environment environment) { putPathConfig(composite); if (this.properties.enableApi()) { - uniqueSources.forEach(s -> { - MapPropertySource propertySource = getSecretsPropertySourceForSingleSecret(env, s); + uniqueSources.forEach(secretSource -> { + MapPropertySource propertySource = getPropertySource(env, secretSource, properties.readType()); if ("true".equals(propertySource.getProperty(Constants.ERROR_PROPERTY))) { - LOG.warn("Failed to load source: " + s); + LOG.warn("Failed to load source: " + secretSource); } else { LOG.debug("Adding secret property source " + propertySource.getName()); @@ -92,7 +89,6 @@ public PropertySource locate(Environment environment) { }); } - cache.discardAll(); return composite; } return null; @@ -103,14 +99,8 @@ public Collection> locateCollection(Environment environment) { return PropertySourceLocator.super.locateCollection(environment); } - private SecretsPropertySource getSecretsPropertySourceForSingleSecret(ConfigurableEnvironment environment, - NormalizedSource normalizedSource) { - - return getPropertySource(environment, normalizedSource); - } - protected abstract SecretsPropertySource getPropertySource(ConfigurableEnvironment environment, - NormalizedSource normalizedSource); + NormalizedSource normalizedSource, ReadType readType); protected void putPathConfig(CompositePropertySource composite) { diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigPropertiesBindingTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigPropertiesBindingTests.java index d46ab8d5fa..0043b4586a 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 @@ -23,6 +23,9 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Configuration; +import static org.springframework.cloud.kubernetes.commons.config.ReadType.BATCH; +import static org.springframework.cloud.kubernetes.commons.config.ReadType.SINGLE; + /** * @author wind57 * @@ -46,6 +49,7 @@ void testWithDefaults() { Assertions.assertThat(props.useNameAsPrefix()).isFalse(); Assertions.assertThat(props.includeProfileSpecificSources()).isTrue(); Assertions.assertThat(props.failFast()).isFalse(); + Assertions.assertThat(props.readType()).isSameAs(BATCH); Assertions.assertThat(props.retry()).isNotNull(); Assertions.assertThat(props.retry().initialInterval()).isEqualTo(1000L); @@ -77,7 +81,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.read-type=SINGLE") .run(context -> { ConfigMapConfigProperties props = context.getBean(ConfigMapConfigProperties.class); Assertions.assertThat(props).isNotNull(); @@ -106,6 +111,7 @@ void testWithNonDefaults() { Assertions.assertThat(props.useNameAsPrefix()).isTrue(); Assertions.assertThat(props.includeProfileSpecificSources()).isTrue(); Assertions.assertThat(props.failFast()).isTrue(); + Assertions.assertThat(props.readType()).isSameAs(SINGLE); RetryProperties retryProperties = props.retry(); Assertions.assertThat(retryProperties).isNotNull(); diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigPropertiesTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/ConfigMapConfigPropertiesTests.java index ea19d6bbeb..75b991b33a 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 @@ -27,6 +27,8 @@ import org.springframework.mock.env.MockEnvironment; +import static org.springframework.cloud.kubernetes.commons.config.ReadType.BATCH; + /** * @author wind57 */ @@ -47,7 +49,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, BATCH); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(1); @@ -73,7 +75,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, BATCH); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(1); @@ -104,7 +106,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, BATCH); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(1); @@ -147,7 +149,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, BATCH); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(3); @@ -198,7 +200,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, BATCH); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(4); @@ -231,7 +233,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, BATCH); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(1); @@ -260,7 +262,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, BATCH); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(1); @@ -304,7 +306,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, BATCH); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(3); @@ -362,7 +364,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, BATCH); 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 cba681e346..264379e224 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 @@ -37,6 +37,8 @@ import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.mock.env.MockEnvironment; +import static org.springframework.cloud.kubernetes.commons.config.ReadType.BATCH; + /** * @author wind57 */ @@ -228,10 +230,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, BATCH); SecretsConfigProperties two = new SecretsConfigProperties(false, Map.of(), List.of(), List.of(), false, null, - null, false, false, false, null); + null, false, false, false, null, ReadType.BATCH); KubernetesClientProperties kubernetesClientProperties = RESOLVER_CONTEXT.getBootstrapContext() .get(KubernetesClientProperties.class); 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 702913bc07..ca90173861 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 @@ -23,6 +23,9 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Configuration; +import static org.springframework.cloud.kubernetes.commons.config.ReadType.BATCH; +import static org.springframework.cloud.kubernetes.commons.config.ReadType.SINGLE; + /** * @author wind57 * @@ -46,6 +49,7 @@ void testWithDefaults() { Assertions.assertThat(props.useNameAsPrefix()).isFalse(); Assertions.assertThat(props.includeProfileSpecificSources()).isTrue(); Assertions.assertThat(props.failFast()).isFalse(); + Assertions.assertThat(props.readType()).isSameAs(BATCH); Assertions.assertThat(props.retry()).isNotNull(); Assertions.assertThat(props.retry().initialInterval()).isEqualTo(1000L); @@ -77,7 +81,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.read-type=SINGLE") .run(context -> { SecretsConfigProperties props = context.getBean(SecretsConfigProperties.class); Assertions.assertThat(props).isNotNull(); @@ -107,6 +112,8 @@ void testWithNonDefaults() { Assertions.assertThat(props.includeProfileSpecificSources()).isTrue(); Assertions.assertThat(props.failFast()).isTrue(); + Assertions.assertThat(props.readType()).isSameAs(SINGLE); + RetryProperties retryProperties = props.retry(); Assertions.assertThat(retryProperties).isNotNull(); Assertions.assertThat(retryProperties.initialInterval()).isEqualTo(1); 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 9d6b0e5c40..b24571dcd8 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SecretsConfigPropertiesTests.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/SecretsConfigPropertiesTests.java @@ -39,7 +39,7 @@ class SecretsConfigPropertiesTests { void emptySourcesSecretName() { SecretsConfigProperties properties = new SecretsConfigProperties(false, Map.of(), List.of(), List.of(), true, - null, "namespace", false, true, false, RetryProperties.DEFAULT); + null, "namespace", false, true, false, RetryProperties.DEFAULT, ReadType.BATCH); List source = properties.determineSources(new MockEnvironment()); Assertions.assertThat(source.size()).isEqualTo(1); @@ -80,7 +80,8 @@ 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, + ReadType.BATCH); List result = properties.determineSources(new MockEnvironment()); Assertions.assertThat(result.size()).isEqualTo(6); @@ -122,7 +123,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, ReadType.BATCH); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(1); @@ -149,7 +150,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, ReadType.BATCH); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(1); @@ -180,7 +181,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, ReadType.BATCH); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(1); @@ -223,7 +224,8 @@ 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, + ReadType.BATCH); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(3); @@ -274,7 +276,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, ReadType.BATCH); List sources = properties.determineSources(new MockEnvironment()); Assertions.assertThat(sources.size()).isEqualTo(4); @@ -335,8 +337,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, + ReadType.BATCH); List sources = properties.determineSources(new MockEnvironment()); // we get 8 property sources, since "named" ones with "application" are 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 87357cf99a..bdb9c0eff1 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 @@ -41,6 +41,7 @@ import org.springframework.cloud.kubernetes.commons.config.NamedConfigMapNormalizedSource; import org.springframework.cloud.kubernetes.commons.config.NamedSecretNormalizedSource; import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; @@ -91,7 +92,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, ReadType.BATCH); propertySources.add(new KubernetesClientConfigMapPropertySource(context)); }); @@ -111,7 +112,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, ReadType.BATCH); 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 d46c52d5da..7ff28a6e9c 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,19 +30,20 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.cloud.config.environment.Environment; import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigContext; import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySource; -import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapsCache; -import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsCache; import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsPropertySource; +import org.springframework.cloud.kubernetes.client.config.KubernetesClientSourcesBatchRead; import org.springframework.cloud.kubernetes.commons.config.ConfigUtils; import org.springframework.cloud.kubernetes.commons.config.Constants; import org.springframework.cloud.kubernetes.commons.config.NamedConfigMapNormalizedSource; import org.springframework.cloud.kubernetes.commons.config.NamedSecretNormalizedSource; import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.core.env.MapPropertySource; import static org.assertj.core.api.Assertions.assertThat; @@ -123,13 +124,13 @@ static void before() { NormalizedSource defaultSource = new NamedConfigMapNormalizedSource(applicationName, "default", false, true); KubernetesClientConfigContext defaultContext = new KubernetesClientConfigContext(coreApi, defaultSource, - "default", springEnv); + "default", springEnv, true, ReadType.BATCH); 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, ReadType.BATCH); propertySources.add(new KubernetesClientConfigMapPropertySource(devContext)); } return propertySources; @@ -139,7 +140,7 @@ static void before() { NormalizedSource source = new NamedSecretNormalizedSource(applicationName, "default", false, true); KubernetesClientConfigContext context = new KubernetesClientConfigContext(coreApi, source, "default", - springEnv); + springEnv, true, ReadType.BATCH); propertySources.add(new KubernetesClientSecretsPropertySource(context)); return propertySources; @@ -147,13 +148,19 @@ static void before() { } @AfterEach - void after() { - new KubernetesClientConfigMapsCache().discardAll(); - new KubernetesClientSecretsCache().discardAll(); + void afterEach() { + KubernetesClientSourcesBatchRead.discardConfigMaps(); + KubernetesClientSourcesBatchRead.discardSecrets(); + } + + @BeforeEach + void beforeEach() { + KubernetesClientSourcesBatchRead.discardConfigMaps(); + KubernetesClientSourcesBatchRead.discardSecrets(); } @Test - public void testApplicationCase() throws ApiException { + void testApplicationCase() throws ApiException { CoreV1Api coreApi = mock(CoreV1Api.class); mockRequests(coreApi); KubernetesEnvironmentRepository environmentRepository = new KubernetesEnvironmentRepository(coreApi, @@ -179,7 +186,7 @@ public void testApplicationCase() throws ApiException { } @Test - public void testApplicationCaseWithNewConstructor() throws ApiException { + void testApplicationCaseWithNewConstructor() throws ApiException { CoreV1Api coreApi = mock(CoreV1Api.class); KubernetesConfigServerProperties properties = mock(KubernetesConfigServerProperties.class); when(properties.getOrder()).thenReturn(0); @@ -211,22 +218,8 @@ public void testApplicationCaseWithNewConstructor() throws ApiException { assertThat(environmentRepository.getOrder()).isEqualTo(0); } - private void mockRequests(CoreV1Api coreApi) throws ApiException { - CoreV1Api.APIlistNamespacedConfigMapRequest defaultConfigRequest = mock( - CoreV1Api.APIlistNamespacedConfigMapRequest.class); - when(defaultConfigRequest.execute()).thenReturn(CONFIGMAP_DEFAULT_LIST); - when(coreApi.listNamespacedConfigMap(eq("default"))).thenReturn(defaultConfigRequest); - CoreV1Api.APIlistNamespacedSecretRequest secretRequest = mock(CoreV1Api.APIlistNamespacedSecretRequest.class); - when(secretRequest.execute()).thenReturn(SECRET_LIST); - when(coreApi.listNamespacedSecret(eq("default"))).thenReturn(secretRequest); - CoreV1Api.APIlistNamespacedConfigMapRequest devConfigRequest = mock( - CoreV1Api.APIlistNamespacedConfigMapRequest.class); - when(devConfigRequest.execute()).thenReturn(CONFIGMAP_DEV_LIST); - when(coreApi.listNamespacedConfigMap(eq("dev"))).thenReturn(devConfigRequest); - } - @Test - public void testStoresCase() throws ApiException { + void testStoresCase() throws ApiException { CoreV1Api coreApi = mock(CoreV1Api.class); mockRequests(coreApi); KubernetesEnvironmentRepository environmentRepository = new KubernetesEnvironmentRepository(coreApi, @@ -265,7 +258,7 @@ public void testStoresCase() throws ApiException { } @Test - public void testStoresProfileCase() throws ApiException { + void testStoresProfileCase() throws ApiException { CoreV1Api coreApi = mock(CoreV1Api.class); mockRequests(coreApi); KubernetesEnvironmentRepository environmentRepository = new KubernetesEnvironmentRepository(coreApi, @@ -321,7 +314,7 @@ else if (propertySource.getName().equals("secret.stores.default")) { } @Test - public void testApplicationPropertiesAnSecretsOverride() throws ApiException { + void testApplicationPropertiesAnSecretsOverride() throws ApiException { CoreV1Api coreApi = mock(CoreV1Api.class); mockRequests(coreApi); KubernetesEnvironmentRepository environmentRepository = new KubernetesEnvironmentRepository(coreApi, @@ -354,7 +347,7 @@ public void testApplicationPropertiesAnSecretsOverride() throws ApiException { } @Test - public void testSingleConfigMapMultipleSources() throws ApiException { + void testSingleConfigMapMultipleSources() throws ApiException { CoreV1Api coreApi = mock(CoreV1Api.class); CoreV1Api.APIlistNamespacedConfigMapRequest configMapRequest = mock( CoreV1Api.APIlistNamespacedConfigMapRequest.class); @@ -369,7 +362,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, ReadType.BATCH); propertySources.add(new KubernetesClientConfigMapPropertySource(devContext)); return propertySources; }); @@ -410,4 +403,18 @@ private static KubernetesConfigServerProperties properties() { return properties; } + private void mockRequests(CoreV1Api coreApi) throws ApiException { + CoreV1Api.APIlistNamespacedConfigMapRequest defaultConfigRequest = mock( + CoreV1Api.APIlistNamespacedConfigMapRequest.class); + when(defaultConfigRequest.execute()).thenReturn(CONFIGMAP_DEFAULT_LIST); + when(coreApi.listNamespacedConfigMap(eq("default"))).thenReturn(defaultConfigRequest); + CoreV1Api.APIlistNamespacedSecretRequest secretRequest = mock(CoreV1Api.APIlistNamespacedSecretRequest.class); + when(secretRequest.execute()).thenReturn(SECRET_LIST); + when(coreApi.listNamespacedSecret(eq("default"))).thenReturn(secretRequest); + CoreV1Api.APIlistNamespacedConfigMapRequest devConfigRequest = mock( + CoreV1Api.APIlistNamespacedConfigMapRequest.class); + when(devConfigRequest.execute()).thenReturn(CONFIGMAP_DEV_LIST); + when(coreApi.listNamespacedConfigMap(eq("dev"))).thenReturn(devConfigRequest); + } + } 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 43e5ae3d82..56ef4ad50a 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 @@ -27,15 +27,13 @@ import io.kubernetes.client.openapi.models.V1Secret; import io.kubernetes.client.openapi.models.V1SecretBuilder; import io.kubernetes.client.openapi.models.V1SecretList; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.cloud.config.environment.Environment; -import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapsCache; -import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsCache; +import org.springframework.cloud.kubernetes.client.config.KubernetesClientSourcesBatchRead; import org.springframework.cloud.kubernetes.commons.config.Constants; import static org.assertj.core.api.Assertions.assertThat; @@ -102,15 +100,16 @@ static void beforeAll() throws ApiException { when(CORE_V1_API.listNamespacedSecret(eq("team-b"))).thenReturn(teamBSecretRequest); } - @AfterAll - static void afterAll() { - Mockito.reset(CORE_V1_API); - } - @AfterEach void afterEach() { - new KubernetesClientConfigMapsCache().discardAll(); - new KubernetesClientSecretsCache().discardAll(); + KubernetesClientSourcesBatchRead.discardConfigMaps(); + KubernetesClientSourcesBatchRead.discardSecrets(); + } + + @BeforeEach + void beforeEach() { + KubernetesClientSourcesBatchRead.discardConfigMaps(); + KubernetesClientSourcesBatchRead.discardSecrets(); } @Test diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/it/CompositeKubernetesIntegrationTests.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/it/CompositeKubernetesIntegrationTests.java index d072e73b23..0b4aaff580 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/it/CompositeKubernetesIntegrationTests.java +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/it/CompositeKubernetesIntegrationTests.java @@ -18,7 +18,6 @@ import java.util.Map; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -27,7 +26,6 @@ import org.springframework.cloud.config.environment.Environment; import org.springframework.cloud.config.environment.PropertySource; import org.springframework.cloud.config.server.environment.NativeEnvironmentRepository; -import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapsCache; import org.springframework.cloud.kubernetes.configserver.KubernetesConfigServerApplication; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; @@ -46,11 +44,6 @@ */ class CompositeKubernetesIntegrationTests { - @AfterEach - void after() { - new KubernetesClientConfigMapsCache().discardAll(); - } - @Nested @SpringBootTest(classes = { KubernetesConfigServerApplication.class }, properties = { "spring.main.cloud-platform=KUBERNETES", "spring.cloud.kubernetes.client.namespace=default", diff --git a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/it/ConfigServerIntegration.java b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/it/ConfigServerIntegration.java index 3d4c0bb1d8..bfa27128f3 100644 --- a/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/it/ConfigServerIntegration.java +++ b/spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configserver/src/test/java/org/springframework/cloud/kubernetes/configserver/it/ConfigServerIntegration.java @@ -36,8 +36,6 @@ import org.springframework.boot.web.server.test.client.TestRestTemplate; import org.springframework.cloud.config.environment.Environment; import org.springframework.cloud.config.environment.PropertySource; -import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapsCache; -import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsCache; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.get; @@ -131,9 +129,6 @@ void afterEach() { WireMock.shutdownServer(); wireMockServer.stop(); wireMockServer.shutdownServer(); - - new KubernetesClientConfigMapsCache().discardAll(); - new KubernetesClientSecretsCache().discardAll(); } @Test diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigContext.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigContext.java index 9cfde97669..78618ec914 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 @@ -19,6 +19,7 @@ import io.fabric8.kubernetes.client.KubernetesClient; import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.core.env.Environment; /** @@ -28,5 +29,5 @@ * @author wind57 */ record Fabric8ConfigContext(KubernetesClient client, NormalizedSource normalizedSource, String namespace, - Environment environment) { + Environment environment, ReadType readType) { } 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 fff4c9e231..077bb2b97e 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigDataLocationResolver.java +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigDataLocationResolver.java @@ -65,7 +65,7 @@ protected void registerBeans(ConfigDataLocationResolverContext resolverContext, kubernetesClient, configMapProperties, namespaceProvider); if (isRetryEnabledForConfigMap(configMapProperties)) { configMapPropertySourceLocator = new ConfigDataRetryableConfigMapPropertySourceLocator( - configMapPropertySourceLocator, configMapProperties, new Fabric8ConfigMapsCache()); + configMapPropertySourceLocator, configMapProperties); } registerSingle(bootstrapContext, ConfigMapPropertySourceLocator.class, configMapPropertySourceLocator, @@ -77,7 +77,7 @@ protected void registerBeans(ConfigDataLocationResolverContext resolverContext, kubernetesClient, secretsProperties, namespaceProvider); if (isRetryEnabledForSecrets(secretsProperties)) { secretsPropertySourceLocator = new ConfigDataRetryableSecretsPropertySourceLocator( - secretsPropertySourceLocator, secretsProperties, new Fabric8SecretsCache()); + secretsPropertySourceLocator, secretsProperties); } 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 0c435874b7..ee7377f8c8 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 @@ -23,11 +23,15 @@ import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties; import org.springframework.cloud.kubernetes.commons.config.ConfigMapPropertySourceLocator; import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.core.annotation.Order; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; import static org.springframework.cloud.kubernetes.fabric8.Fabric8Utils.getApplicationNamespace; +import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8SourcesBatchRead.discardConfigMaps; /** * A {@link PropertySourceLocator} that uses config maps. @@ -45,19 +49,27 @@ public class Fabric8ConfigMapPropertySourceLocator extends ConfigMapPropertySour Fabric8ConfigMapPropertySourceLocator(KubernetesClient client, ConfigMapConfigProperties properties, KubernetesNamespaceProvider provider) { - super(properties, new Fabric8ConfigMapsCache()); + super(properties); this.client = client; this.provider = provider; } + @Override + public PropertySource locate(Environment environment) { + PropertySource configMapSources = super.locate(environment); + discardConfigMaps(); + return configMapSources; + } + @Override protected MapPropertySource getMapPropertySource(NormalizedSource normalizedSource, - ConfigurableEnvironment environment) { + ConfigurableEnvironment environment, ReadType readType) { // 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, + readType); return new Fabric8ConfigMapPropertySource(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/Fabric8ConfigMapsCache.java deleted file mode 100644 index 836da49a41..0000000000 --- a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapsCache.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2013-present 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.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.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 Fabric8ConfigMapsCache implements ConfigMapCache { - - private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(Fabric8ConfigMapsCache.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 strippedConfigMaps(client.configMaps().inNamespace(namespace).list().getItems()); - }); - - if (b[0]) { - LOG.debug(() -> "Loaded all config maps in namespace '" + namespace + "'"); - } - else { - LOG.debug(() -> "Loaded (from cache) all config maps in namespace '" + namespace + "'"); - } - - return result; - } - - private static List strippedConfigMaps(List configMaps) { - return configMaps.stream() - .map(configMap -> new StrippedSourceContainer(configMap.getMetadata().getLabels(), - configMap.getMetadata().getName(), configMap.getData())) - .toList(); - } - -} 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 710ab98eee..25d8133cf0 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 @@ -21,17 +21,26 @@ import java.util.Map; import java.util.Set; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.client.KubernetesClient; -import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; -import org.springframework.cloud.kubernetes.commons.config.ConfigUtils; import org.springframework.cloud.kubernetes.commons.config.MultipleSourcesContainer; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.cloud.kubernetes.commons.config.StrippedSourceContainer; import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; -import org.springframework.cloud.kubernetes.fabric8.Fabric8Utils; import org.springframework.core.env.Environment; +import org.springframework.core.log.LogAccessor; + +import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.processLabeledData; +import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.processNamedData; +import static org.springframework.cloud.kubernetes.fabric8.Fabric8Utils.getApplicationNamespace; +import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8SourcesBatchRead.strippedConfigMaps; +import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8SourcesBatchRead.strippedSecrets; +import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8SourcesSingleRead.strippedConfigMaps; +import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8SourcesSingleRead.strippedSecrets; /** * Utility class that works with configuration properties. @@ -40,7 +49,7 @@ */ public final class Fabric8ConfigUtils { - private static final Log LOG = LogFactory.getLog(Fabric8ConfigUtils.class); + private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(Fabric8ConfigUtils.class)); private Fabric8ConfigUtils() { } @@ -52,45 +61,36 @@ public static Set namespaces(KubernetesClient client, KubernetesNamespac ConfigReloadProperties properties, String target) { Set namespaces = properties.namespaces(); if (namespaces.isEmpty()) { - namespaces = Set.of(Fabric8Utils.getApplicationNamespace(client, null, target, provider)); + namespaces = Set.of(getApplicationNamespace(client, null, target, provider)); } LOG.debug("informer namespaces : " + namespaces); 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. see if any of the secrets from (4) has a single yaml/properties file
-	 *     4. gather all the names of the secrets (from 4) + data they hold
-	 * 
- */ - static MultipleSourcesContainer secretsDataByLabels(KubernetesClient client, String namespace, - Map labels, Environment environment) { - List strippedSecrets = strippedSecrets(client, namespace); - if (strippedSecrets.isEmpty()) { - return MultipleSourcesContainer.empty(); - } - return ConfigUtils.processLabeledData(strippedSecrets, environment, labels, namespace, 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. see if any from (4) has a single yaml/properties file
-	 *     4. 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) { - List strippedConfigMaps = strippedConfigMaps(client, namespace); - if (strippedConfigMaps.isEmpty()) { - return MultipleSourcesContainer.empty(); + static MultipleSourcesContainer configMapsByName(KubernetesClient client, String namespace, + LinkedHashSet sourceNames, Environment environment, ReadType readType) { + + List strippedConfigMaps; + + if (readType.equals(ReadType.BATCH)) { + LOG.debug(() -> "Will read all configmaps in namespace : " + namespace); + strippedConfigMaps = strippedConfigMaps(client, namespace); + } + else { + LOG.debug(() -> "Will read individual configmaps in namespace : " + namespace + " with names : " + + sourceNames); + strippedConfigMaps = strippedConfigMaps(client, namespace, sourceNames); } - return ConfigUtils.processLabeledData(strippedConfigMaps, environment, labels, namespace, false); + return processNamedData(strippedConfigMaps, environment, sourceNames, namespace, false, true); } /** @@ -101,48 +101,86 @@ static MultipleSourcesContainer configMapsDataByLabels(KubernetesClient client, * 4. gather all the names of the secrets + decoded data they hold * */ - static MultipleSourcesContainer secretsDataByName(KubernetesClient client, String namespace, - LinkedHashSet sourceNames, Environment environment) { - List strippedSecrets = strippedSecrets(client, namespace); - if (strippedSecrets.isEmpty()) { - return MultipleSourcesContainer.empty(); + static MultipleSourcesContainer secretsByName(KubernetesClient client, String namespace, + LinkedHashSet sourceNames, Environment environment, ReadType readType) { + + List strippedSecrets; + + if (readType.equals(ReadType.BATCH)) { + LOG.debug(() -> "Will read all secrets in namespace : " + namespace); + strippedSecrets = strippedSecrets(client, namespace); + } + else { + LOG.debug( + () -> "Will read individual secrets in namespace : " + namespace + " with names : " + sourceNames); + strippedSecrets = strippedSecrets(client, namespace, sourceNames); } - return ConfigUtils.processNamedData(strippedSecrets, environment, sourceNames, namespace, true, true); + + return processNamedData(strippedSecrets, environment, sourceNames, namespace, true, 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(); + static MultipleSourcesContainer configMapsByLabels(KubernetesClient client, String namespace, + Map labels, Environment environment, ReadType readType) { + + List strippedConfigMaps; + + if (readType.equals(ReadType.BATCH)) { + LOG.debug(() -> "Will read all configmaps in namespace : " + namespace); + strippedConfigMaps = strippedConfigMaps(client, namespace); + } + else { + LOG.debug(() -> "Will read individual configmaps in namespace : " + namespace + " with labels : " + labels); + strippedConfigMaps = strippedConfigMaps(client, namespace, labels); } - return ConfigUtils.processNamedData(strippedConfigMaps, environment, sourceNames, namespace, false, true); + + return processLabeledData(strippedConfigMaps, environment, labels, namespace, false); } - private static List strippedConfigMaps(KubernetesClient client, String namespace) { - List strippedConfigMaps = Fabric8ConfigMapsCache.byNamespace(client, namespace); - if (strippedConfigMaps.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 secretsByLabels(KubernetesClient client, String namespace, + Map labels, Environment environment, ReadType readType) { + + List strippedSecrets; + + if (readType.equals(ReadType.BATCH)) { + LOG.debug(() -> "Will read all secrets in namespace : " + namespace); + strippedSecrets = strippedSecrets(client, namespace); + } + else { + LOG.debug(() -> "Will read individual secrets in namespace : " + namespace + " with labels : " + labels); + strippedSecrets = strippedSecrets(client, namespace, labels); } - return strippedConfigMaps; + return processLabeledData(strippedSecrets, environment, labels, namespace, true); } - private static List strippedSecrets(KubernetesClient client, String namespace) { - List strippedSecrets = Fabric8SecretsCache.byNamespace(client, namespace); - if (strippedSecrets.isEmpty()) { - LOG.debug("No secrets in namespace '" + namespace + "'"); - } + static List stripConfigMaps(List configMaps) { + return configMaps.stream() + .map(configMap -> new StrippedSourceContainer(configMap.getMetadata().getLabels(), + configMap.getMetadata().getName(), configMap.getData())) + .toList(); + } - return strippedSecrets; + static List stripSecrets(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/Fabric8SecretsCache.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsCache.java deleted file mode 100644 index 4233f29c8c..0000000000 --- a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsCache.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2013-present 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 f838432afe..9564722e9e 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 @@ -21,13 +21,17 @@ import org.springframework.cloud.bootstrap.config.PropertySourceLocator; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties; import org.springframework.cloud.kubernetes.commons.config.SecretsPropertySource; import org.springframework.cloud.kubernetes.commons.config.SecretsPropertySourceLocator; import org.springframework.core.annotation.Order; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertySource; import static org.springframework.cloud.kubernetes.fabric8.Fabric8Utils.getApplicationNamespace; +import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8SourcesBatchRead.discardSecrets; /** * Kubernetes {@link PropertySourceLocator} for secrets. @@ -45,19 +49,27 @@ public class Fabric8SecretsPropertySourceLocator extends SecretsPropertySourceLo Fabric8SecretsPropertySourceLocator(KubernetesClient client, SecretsConfigProperties properties, KubernetesNamespaceProvider provider) { - super(properties, new Fabric8SecretsCache()); + super(properties); this.client = client; this.provider = provider; } + @Override + public PropertySource locate(Environment environment) { + PropertySource propertySource = super.locate(environment); + discardSecrets(); + return propertySource; + } + @Override protected SecretsPropertySource getPropertySource(ConfigurableEnvironment environment, - NormalizedSource normalizedSource) { + NormalizedSource normalizedSource, ReadType readType) { // 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, + readType); return new Fabric8SecretsPropertySource(context); } diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SourcesBatchRead.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SourcesBatchRead.java new file mode 100644 index 0000000000..eb1e77afcd --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SourcesBatchRead.java @@ -0,0 +1,67 @@ +/* + * Copyright 2013-present 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.client.KubernetesClient; + +import org.springframework.cloud.kubernetes.commons.config.StrippedSourceContainer; + +import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigUtils.stripConfigMaps; +import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigUtils.stripSecrets; + +/** + * A cache of ConfigMaps / Secrets per namespace. Makes sure we read only once from a + * namespace. + * + * @author wind57 + */ +final class Fabric8SourcesBatchRead { + + private Fabric8SourcesBatchRead() { + + } + + /** + * 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> SECRETS_CACHE = new ConcurrentHashMap<>(); + + private static final ConcurrentHashMap> CONFIGMAPS_CACHE = new ConcurrentHashMap<>(); + + static void discardSecrets() { + SECRETS_CACHE.clear(); + } + + static void discardConfigMaps() { + CONFIGMAPS_CACHE.clear(); + } + + static List strippedConfigMaps(KubernetesClient client, String namespace) { + return CONFIGMAPS_CACHE.computeIfAbsent(namespace, + x -> stripConfigMaps(client.configMaps().inNamespace(namespace).list().getItems())); + } + + static List strippedSecrets(KubernetesClient client, String namespace) { + return SECRETS_CACHE.computeIfAbsent(namespace, + x -> stripSecrets(client.secrets().inNamespace(namespace).list().getItems())); + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SourcesSingleRead.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SourcesSingleRead.java new file mode 100644 index 0000000000..986385ed2d --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SourcesSingleRead.java @@ -0,0 +1,136 @@ +/* + * Copyright 2013-present 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; + +import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigUtils.stripConfigMaps; +import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigUtils.stripSecrets; + +/** + * non batch reads (not reading in the whole namespace) of configmaps and secrets. + * + * @author wind57 + */ +final class Fabric8SourcesSingleRead { + + private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(Fabric8SourcesSingleRead.class)); + + private Fabric8SourcesSingleRead() { + + } + + /** + * read configmaps by name, one by one, without caching them. + */ + static List strippedConfigMaps(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 = stripConfigMaps(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 strippedSecrets(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 = stripSecrets(secrets); + + if (strippedSecrets.isEmpty()) { + LOG.debug(() -> "No secrets in namespace '" + namespace + "'"); + } + + return strippedSecrets; + } + + /** + * read configmaps by labels, without caching them. + */ + static List strippedConfigMaps(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 = stripConfigMaps(configMaps); + if (strippedConfigMaps.isEmpty()) { + LOG.debug(() -> "No configmaps in namespace '" + namespace + "'"); + } + + return strippedConfigMaps; + } + + /** + * read secrets by labels, without caching them. + */ + static List strippedSecrets(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 = stripSecrets(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/LabeledConfigMapContextToSourceDataProvider.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledConfigMapContextToSourceDataProvider.java index e080b6d2db..59659a8a6f 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 @@ -23,6 +23,8 @@ import org.springframework.cloud.kubernetes.commons.config.LabeledSourceData; import org.springframework.cloud.kubernetes.commons.config.MultipleSourcesContainer; +import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigUtils.configMapsByLabels; + /** * Provides an implementation of {@link Fabric8ContextToSourceData} for a labeled config * map. @@ -55,8 +57,8 @@ public Fabric8ContextToSourceData get() { return new LabeledSourceData() { @Override public MultipleSourcesContainer dataSupplier(Map labels) { - return Fabric8ConfigUtils.configMapsDataByLabels(context.client(), context.namespace(), labels, - context.environment()); + return configMapsByLabels(context.client(), context.namespace(), labels, context.environment(), + context.readType()); } }.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 eb2c8d3a32..597ac71dfb 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 @@ -23,6 +23,8 @@ import org.springframework.cloud.kubernetes.commons.config.LabeledSourceData; import org.springframework.cloud.kubernetes.commons.config.MultipleSourcesContainer; +import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigUtils.secretsByLabels; + /** * Provides an implementation of {@link Fabric8ContextToSourceData} for a labeled secret. * @@ -54,8 +56,8 @@ public Fabric8ContextToSourceData get() { return new LabeledSourceData() { @Override public MultipleSourcesContainer dataSupplier(Map labels) { - return Fabric8ConfigUtils.secretsDataByLabels(context.client(), context.namespace(), labels, - context.environment()); + return secretsByLabels(context.client(), context.namespace(), labels, context.environment(), + context.readType()); } }.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 760349a678..c7071ba6a4 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 @@ -19,11 +19,13 @@ import java.util.LinkedHashSet; import java.util.function.Supplier; -import org.springframework.cloud.kubernetes.commons.config.ConfigUtils; import org.springframework.cloud.kubernetes.commons.config.MultipleSourcesContainer; import org.springframework.cloud.kubernetes.commons.config.NamedConfigMapNormalizedSource; import org.springframework.cloud.kubernetes.commons.config.NamedSourceData; +import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.sourceName; +import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigUtils.configMapsByName; + /** * Provides an implementation of {@link Fabric8ContextToSourceData} for a named config * map. @@ -55,15 +57,15 @@ public Fabric8ContextToSourceData get() { protected String generateSourceName(String target, String sourceName, String namespace, String[] activeProfiles) { if (source.appendProfileToName()) { - return ConfigUtils.sourceName(target, sourceName, namespace, activeProfiles); + return sourceName(target, sourceName, namespace, activeProfiles); } return super.generateSourceName(target, sourceName, namespace, activeProfiles); } @Override public MultipleSourcesContainer dataSupplier(LinkedHashSet sourceNames) { - return Fabric8ConfigUtils.configMapsDataByName(context.client(), context.namespace(), sourceNames, - context.environment()); + return configMapsByName(context.client(), context.namespace(), sourceNames, context.environment(), + context.readType()); } }.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 b6c8e35ecc..c1a2843f20 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 @@ -19,11 +19,13 @@ import java.util.LinkedHashSet; import java.util.function.Supplier; -import org.springframework.cloud.kubernetes.commons.config.ConfigUtils; import org.springframework.cloud.kubernetes.commons.config.MultipleSourcesContainer; import org.springframework.cloud.kubernetes.commons.config.NamedSecretNormalizedSource; import org.springframework.cloud.kubernetes.commons.config.NamedSourceData; +import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.sourceName; +import static org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigUtils.secretsByName; + /** * Provides an implementation of {@link Fabric8ContextToSourceData} for a named secret. * @@ -45,15 +47,15 @@ public Fabric8ContextToSourceData get() { protected String generateSourceName(String target, String sourceName, String namespace, String[] activeProfiles) { if (source.appendProfileToName()) { - return ConfigUtils.sourceName(target, sourceName, namespace, activeProfiles); + return sourceName(target, sourceName, namespace, activeProfiles); } return super.generateSourceName(target, sourceName, namespace, activeProfiles); } @Override public MultipleSourcesContainer dataSupplier(LinkedHashSet sourceNames) { - return Fabric8ConfigUtils.secretsDataByName(context.client(), context.namespace(), sourceNames, - context.environment()); + return secretsByName(context.client(), context.namespace(), sourceNames, context.environment(), + context.readType()); } }.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 cc8e2055d9..7445963512 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 @@ -23,11 +23,13 @@ import io.fabric8.kubernetes.api.model.ConfigMapList; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.springframework.cloud.kubernetes.commons.config.NamedConfigMapNormalizedSource; import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.mock.env.MockEnvironment; import static org.assertj.core.api.Assertions.assertThat; @@ -44,7 +46,7 @@ class ConfigMapsTest { @AfterEach void afterEach() { - new Fabric8ConfigMapsCache().discardAll(); + Fabric8SourcesBatchRead.discardConfigMaps(); } @Test @@ -91,7 +93,8 @@ 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(), + ReadType.BATCH); Fabric8ConfigMapPropertySource cmps = new Fabric8ConfigMapPropertySource(context); @@ -111,7 +114,8 @@ 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(), + ReadType.BATCH); Fabric8ConfigMapPropertySource cmps = new Fabric8ConfigMapPropertySource(context); @@ -131,7 +135,8 @@ 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(), + ReadType.BATCH); Fabric8ConfigMapPropertySource cmps = new Fabric8ConfigMapPropertySource(context); @@ -151,7 +156,8 @@ 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(), + ReadType.BATCH); Fabric8ConfigMapPropertySource cmps = new Fabric8ConfigMapPropertySource(context); @@ -169,9 +175,10 @@ 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(), + ReadType.BATCH); - Fabric8ConfigMapPropertySource cmps = new Fabric8ConfigMapPropertySource(context); + Assertions.assertThatCode(() -> new Fabric8ConfigMapPropertySource(context)).doesNotThrowAnyException(); // no exception is thrown for unparseable content } @@ -188,7 +195,8 @@ 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, "test", new MockEnvironment(), + ReadType.BATCH); Fabric8ConfigMapPropertySource cmps = new Fabric8ConfigMapPropertySource(context); diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapErrorOnReadingSourceTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapErrorOnReadingSourceTests.java index 014604484a..3b9bbdf84d 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapErrorOnReadingSourceTests.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapErrorOnReadingSourceTests.java @@ -33,6 +33,7 @@ import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.cloud.kubernetes.commons.config.RetryProperties; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.PropertySource; @@ -72,7 +73,7 @@ void namedSingleConfigMapFails(CapturedOutput output) { mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once(); ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(), - Map.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT); + Map.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT, ReadType.BATCH); Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -108,7 +109,7 @@ void namedTwoConfigMapsOneFails(CapturedOutput output) { ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(sourceOne, sourceTwo), Map.of(), true, null, namespace, false, true, false, - RetryProperties.DEFAULT); + RetryProperties.DEFAULT, ReadType.BATCH); Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -142,7 +143,7 @@ void namedTwoConfigMapsBothFail(CapturedOutput output) { ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(sourceOne, sourceTwo), Map.of(), true, null, namespace, false, true, false, - RetryProperties.DEFAULT); + RetryProperties.DEFAULT, ReadType.BATCH); Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -173,7 +174,8 @@ void labeledSingleConfigMapFails(CapturedOutput output) { Source configMapSource = new Source(null, namespace, labels, null, null, null); ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), - List.of(configMapSource), labels, true, null, namespace, false, true, false, RetryProperties.DEFAULT); + List.of(configMapSource), labels, true, null, namespace, false, true, false, RetryProperties.DEFAULT, + ReadType.BATCH); Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -217,7 +219,7 @@ void labeledTwoConfigMapsOneFails(CapturedOutput output) { ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(sourceOne, sourceTwo), Map.of("one", "1", "two", "2"), true, null, namespace, false, true, - false, RetryProperties.DEFAULT); + false, RetryProperties.DEFAULT, ReadType.BATCH); Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -256,7 +258,7 @@ void labeledTwoConfigMapsBothFail(CapturedOutput output) { ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(sourceOne, sourceTwo), Map.of("one", "1", "two", "2"), true, null, namespace, false, true, - false, RetryProperties.DEFAULT); + false, RetryProperties.DEFAULT, ReadType.BATCH); Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocatorMockTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigMapPropertySourceLocatorMockTests.java index 7f0f882ad1..1bc1cee9b1 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 @@ -29,6 +29,7 @@ import org.springframework.cloud.kubernetes.commons.config.NamedConfigMapNormalizedSource; import org.springframework.cloud.kubernetes.commons.config.NamespaceResolutionFailedException; import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.cloud.kubernetes.commons.config.RetryProperties; import org.springframework.mock.env.MockEnvironment; @@ -47,13 +48,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, ReadType.BATCH); 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(), ReadType.BATCH)) .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 40d77f0671..b9cad8cf82 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 @@ -31,6 +31,7 @@ import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.cloud.kubernetes.commons.config.RetryProperties; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.PropertySource; @@ -65,7 +66,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, ReadType.BATCH); Fabric8ConfigMapPropertySourceLocator locator = new Fabric8ConfigMapPropertySourceLocator(mockClient, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -83,7 +84,7 @@ void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled(CapturedOutput mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").always(); ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(), - Map.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT); + Map.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT, ReadType.BATCH); 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 fad0d125c3..6b45770699 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 @@ -22,6 +22,7 @@ import org.springframework.cloud.kubernetes.commons.config.NamedConfigMapNormalizedSource; import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.mock.env.MockEnvironment; import static org.assertj.core.api.Assertions.assertThat; @@ -38,7 +39,8 @@ 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(), + ReadType.BATCH); assertThat(new Fabric8ConfigMapPropertySource(context)).isNotNull(); } @@ -47,7 +49,8 @@ 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(), + ReadType.BATCH); 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 ffa94bb592..d157a45458 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 @@ -26,6 +26,7 @@ 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.ReadType; import org.springframework.mock.env.MockEnvironment; import static org.assertj.core.api.Assertions.assertThatNoException; @@ -50,7 +51,7 @@ void beforeEach() { @AfterEach void afterEach() { - new Fabric8ConfigMapsCache().discardAll(); + Fabric8SourcesBatchRead.discardConfigMaps(); } @Test @@ -61,7 +62,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(), + ReadType.BATCH); assertThatThrownBy(() -> new Fabric8ConfigMapPropertySource(context)).isInstanceOf(IllegalStateException.class) .hasMessageContaining("v1/namespaces/default/configmaps. Message: Internal Server Error."); } @@ -74,7 +76,8 @@ void constructorShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() { mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").always(); NormalizedSource source = new NamedConfigMapNormalizedSource(name, namespace, false, false); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "default", new MockEnvironment()); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, source, "default", new MockEnvironment(), + ReadType.BATCH); assertThatNoException().isThrownBy(() -> new Fabric8ConfigMapPropertySource(context)); } diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigUtilsBatchReadTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigUtilsBatchReadTests.java new file mode 100644 index 0000000000..3d78deea5f --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigUtilsBatchReadTests.java @@ -0,0 +1,407 @@ +/* + * Copyright 2013-present 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.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.kubernetes.commons.config.MultipleSourcesContainer; +import org.springframework.cloud.kubernetes.commons.config.ReadType; +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 Fabric8ConfigUtilsBatchReadTests { + + private KubernetesClient client; + + @AfterEach + void afterEach() { + Fabric8SourcesBatchRead.discardSecrets(); + Fabric8SourcesBatchRead.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.secretsByLabels(client, "spring-k8s", + Map.of("color", "red"), new MockEnvironment(), ReadType.BATCH); + Assertions.assertThat(result.data()).isEmpty(); + Assertions.assertThat(result.data().keySet()).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.secretsByLabels(client, "spring-k8s", + Map.of("color", "pink"), new MockEnvironment(), ReadType.BATCH); + Assertions.assertThat(result.data().keySet()).containsExactlyInAnyOrder("my-secret"); + + Map data = result.data().get("my-secret"); + Assertions.assertThat(data).containsExactlyInAnyOrderEntriesOf(Map.of("property", "value")); + } + + /** + *
+	 * 		- secret 'my-secret' is deployed with label '{color:pink}'
+	 * 		- we search for it by same label and find it.
+	 * 		- This secret contains a single .yaml property, as such, it gets some special treatment.
+	 * 
+ */ + @Test + void testSecretDataByLabelsSecretFoundWithPropertyFile() { + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("my-secret").withLabels(Map.of("color", "pink")).build()) + .addToData(Map.of(APPLICATION_YAML, Base64.getEncoder().encodeToString("key1: value1".getBytes()))) + .build()) + .create(); + + MultipleSourcesContainer result = Fabric8ConfigUtils.secretsByLabels(client, "spring-k8s", + Map.of("color", "pink"), new MockEnvironment(), ReadType.BATCH); + Assertions.assertThat(result.data().keySet()).containsExactlyInAnyOrder("my-secret"); + + Map data = result.data().get("my-secret"); + Assertions.assertThat(data).containsExactlyInAnyOrderEntriesOf(Map.of("key1", "value1")); + } + + /** + *
+	 * 		- secrets 'my-secret' and 'my-secret-2' are deployed with label {color:pink}
+	 * 		- we search for them by same label and find them.
+	 * 
+ */ + @Test + void testSecretDataByLabelsTwoSecretsFound() { + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("my-secret").withLabels(Map.of("color", "pink")).build()) + .addToData(Map.of("property", Base64.getEncoder().encodeToString("value".getBytes()))) + .build()) + .create(); + + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata( + new ObjectMetaBuilder().withName("my-secret-2").withLabels(Map.of("color", "pink")).build()) + .addToData(Map.of("property-2", Base64.getEncoder().encodeToString("value-2".getBytes()))) + .build()) + .create(); + + MultipleSourcesContainer result = Fabric8ConfigUtils.secretsByLabels(client, "spring-k8s", + Map.of("color", "pink"), new MockEnvironment(), ReadType.BATCH); + Assertions.assertThat(result.data().keySet()).contains("my-secret"); + Assertions.assertThat(result.data().keySet()).contains("my-secret-2"); + + LinkedHashMap> data = result.data(); + Assertions.assertThat(data).hasSize(2); + Map mySecretData = result.data().get("my-secret"); + Assertions.assertThat(mySecretData.get("property")).isEqualTo("value"); + + Map mySecretData2 = result.data().get("my-secret-2"); + Assertions.assertThat(mySecretData2.get("property-2")).isEqualTo("value-2"); + } + + /** + *
+	 *     - secret deployed with name "blue-circle-secret" and labels "color=blue, shape=circle, tag=fit"
+	 *     - secret deployed with name "blue-square-secret" and labels "color=blue, shape=square, tag=fit"
+	 *     - secret deployed with name "blue-triangle-secret" and labels "color=blue, shape=triangle, tag=no-fit"
+	 *     - secret deployed with name "blue-square-secret-k8s" and labels "color=blue, shape=triangle, tag=no-fit"
+	 *
+	 *     - we search by labels "color=blue, tag=fits", as such find two secrets: "blue-circle-secret"
+	 *       and "blue-square-secret".
+	 * 
+ */ + @Test + void testSecretDataByLabelsThreeSecretsFound() { + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("blue-circle-secret") + .withLabels(Map.of("color", "blue", "shape", "circle", "tag", "fit")) + .build()) + .addToData(Map.of("one", Base64.getEncoder().encodeToString("1".getBytes()))) + .build()) + .create(); + + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("blue-square-secret") + .withLabels(Map.of("color", "blue", "shape", "square", "tag", "fit")) + .build()) + .addToData(Map.of("two", Base64.getEncoder().encodeToString("2".getBytes()))) + .build()) + .create(); + + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("blue-triangle-secret") + .withLabels(Map.of("color", "blue", "shape", "triangle", "tag", "no-fit")) + .build()) + .addToData(Map.of("three", Base64.getEncoder().encodeToString("3".getBytes()))) + .build()) + .create(); + + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("blue-square-secret-k8s") + .withLabels(Map.of("color", "blue", "shape", "triangle", "tag", "no-fit")) + .build()) + .addToData(Map.of("four", Base64.getEncoder().encodeToString("4".getBytes()))) + .build()) + .create(); + + MultipleSourcesContainer result = Fabric8ConfigUtils.secretsByLabels(client, "spring-k8s", + Map.of("tag", "fit", "color", "blue"), new MockEnvironment(), ReadType.BATCH); + + Assertions.assertThat(result.data().keySet()).contains("blue-circle-secret"); + Assertions.assertThat(result.data().keySet()).contains("blue-square-secret"); + + Assertions.assertThat(result.data()).hasSize(2); + + Map dataOne = result.data().get("blue-circle-secret"); + Assertions.assertThat(dataOne).containsExactlyInAnyOrderEntriesOf(Map.of("one", "1")); + + Map dataTwo = result.data().get("blue-square-secret"); + Assertions.assertThat(dataTwo).containsExactlyInAnyOrderEntriesOf(Map.of("two", "2")); + + } + + /** + *
+	 * 		- secret 'my-secret' is deployed; we search for it by name and do not find it.
+	 * 
+ */ + @Test + void testSecretDataByNameSecretNotFound() { + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder().withMetadata(new ObjectMetaBuilder().withName("my-secret").build()).build()) + .create(); + LinkedHashSet names = new LinkedHashSet<>(); + names.add("nope"); + MultipleSourcesContainer result = Fabric8ConfigUtils.secretsByName(client, "spring-k8s", names, + new MockEnvironment(), ReadType.BATCH); + Assertions.assertThat(result.data().keySet()).isEmpty(); + Assertions.assertThat(result.data()).isEmpty(); + } + + /** + *
+	 * 		- secret "my-secret" is deployed; we search for it by name and find it.
+	 * 
+ */ + @Test + void testSecretDataByNameSecretFound() { + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder().withMetadata(new ObjectMetaBuilder().withName("my-secret").build()) + .addToData(Map.of("property", Base64.getEncoder().encodeToString("value".getBytes()))) + .build()) + .create(); + LinkedHashSet names = new LinkedHashSet<>(); + names.add("my-secret"); + + MultipleSourcesContainer result = Fabric8ConfigUtils.secretsByName(client, "spring-k8s", names, + new MockEnvironment(), ReadType.BATCH); + Assertions.assertThat(result.data().keySet()).hasSize(1); + + Map data = result.data().get("my-secret"); + Assertions.assertThat(data.get("property")).isEqualTo("value"); + } + + /** + *
+	 * 		- config-map "my-config-map" is deployed without any data
+	 * 		- we search for it by name and find it; but it has no data.
+	 * 
+ */ + @Test + void testConfigMapsDataByNameFoundNo() { + 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.configMapsByName(client, "spring-k8s", names, + new MockEnvironment(), ReadType.BATCH); + Assertions.assertThat(result.data().keySet()).containsExactlyInAnyOrder("my-config-map"); + + Map data = result.data().get("my-config-map"); + Assertions.assertThat(data).isEmpty(); + } + + /** + *
+	 *     	- config-map "my-config-map" is deployed; we search for it and do not find it.
+	 * 
+ */ + @Test + void testConfigMapsByNameNotFound() { + 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.configMapsByName(client, "spring-k8s", names, + new MockEnvironment(), ReadType.BATCH); + Assertions.assertThat(result.data().keySet()).isEmpty(); + Assertions.assertThat(result.data()).isEmpty(); + } + + /** + *
+	 *     - config-map "my-config-map" is deployed; we search for it and find it
+	 * 
+ */ + @Test + void testConfigMapDataByNameFound() { + client.configMaps() + .inNamespace("spring-k8s") + .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) + .addToData(Map.of("property", "value")) + .build()) + .create(); + + LinkedHashSet names = new LinkedHashSet<>(); + names.add("my-config-map"); + + MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsByName(client, "spring-k8s", names, + new MockEnvironment(), ReadType.BATCH); + Assertions.assertThat(result.data().keySet()).containsExactlyInAnyOrder("my-config-map"); + + Map data = result.data().get("my-config-map"); + Assertions.assertThat(data).containsExactlyInAnyOrderEntriesOf(Map.of("property", "value")); + } + + /** + *
+	 *     - config-map "my-config-map" is deployed
+	 *     - we search for it and find it
+	 *     - it contains a single .yaml property, as such it gets some special treatment
+	 * 
+ */ + @Test + void testConfigMapDataByNameFoundWithPropertyFile() { + client.configMaps() + .inNamespace("spring-k8s") + .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) + .addToData(Map.of(APPLICATION_YAML, "key1: value1")) + .build()) + .create(); + + LinkedHashSet names = new LinkedHashSet<>(); + names.add("my-config-map"); + + MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsByName(client, "spring-k8s", names, + new MockEnvironment(), ReadType.BATCH); + Assertions.assertThat(result.data().keySet()).containsExactlyInAnyOrder("my-config-map"); + + Map data = result.data().get("my-config-map"); + Assertions.assertThat(data).containsExactlyInAnyOrderEntriesOf(Map.of("key1", "value1")); + } + + /** + *
+	 *     - config-map "my-config-map" and "my-config-map-2" are deployed
+	 *     - we search and find them.
+	 * 
+ */ + @Test + void testConfigMapDataByNameTwoFound() { + client.configMaps() + .inNamespace("spring-k8s") + .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) + .addToData(Map.of("property", "value")) + .build()) + .create(); + + client.configMaps() + .inNamespace("spring-k8s") + .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map-2").build()) + .addToData(Map.of("property-2", "value-2")) + .build()) + .create(); + + LinkedHashSet names = new LinkedHashSet<>(); + names.add("my-config-map"); + names.add("my-config-map-2"); + + MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsByName(client, "spring-k8s", names, + new MockEnvironment(), ReadType.BATCH); + Assertions.assertThat(result.data().keySet()).contains("my-config-map"); + Assertions.assertThat(result.data().keySet()).contains("my-config-map-2"); + + Assertions.assertThat(result.data()).hasSize(2); + + Map data = result.data().get("my-config-map"); + Assertions.assertThat(data).contains(Map.entry("property", "value")); + + Map data2 = result.data().get("my-config-map-2"); + Assertions.assertThat(data2).contains(Map.entry("property-2", "value-2")); + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigUtilsSingleReadTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigUtilsSingleReadTests.java new file mode 100644 index 0000000000..fea36e7da6 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigUtilsSingleReadTests.java @@ -0,0 +1,406 @@ +/* + * Copyright 2013-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.config; + +import java.util.Base64; +import java.util.LinkedHashSet; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.kubernetes.commons.config.MultipleSourcesContainer; +import org.springframework.cloud.kubernetes.commons.config.ReadType; +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 Fabric8ConfigUtilsSingleReadTests { + + private KubernetesClient client; + + @AfterEach + void afterEach() { + Fabric8SourcesBatchRead.discardSecrets(); + Fabric8SourcesBatchRead.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.secretsByLabels(client, "spring-k8s", + Map.of("color", "red"), new MockEnvironment(), ReadType.SINGLE); + Assertions.assertThat(result.data()).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.secretsByLabels(client, "spring-k8s", + Map.of("color", "pink"), new MockEnvironment(), ReadType.SINGLE); + Assertions.assertThat(result.data().keySet()).containsExactlyInAnyOrder("my-secret"); + + Map data = result.data().get("my-secret"); + Assertions.assertThat(data.get("property")).isEqualTo("value"); + + } + + /** + *
+	 * 		- secret 'my-secret' is deployed with label '{color:pink}'
+	 * 		- we search for it by same label and find it.
+	 * 		- This secret contains a single .yaml property, as such, it gets some special treatment.
+	 * 
+ */ + @Test + void testSecretDataByLabelsSecretFoundWithPropertyFile() { + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("my-secret").withLabels(Map.of("color", "pink")).build()) + .addToData(Map.of(APPLICATION_YAML, Base64.getEncoder().encodeToString("key1: value1".getBytes()))) + .build()) + .create(); + + MultipleSourcesContainer result = Fabric8ConfigUtils.secretsByLabels(client, "spring-k8s", + Map.of("color", "pink"), new MockEnvironment(), ReadType.SINGLE); + Assertions.assertThat(result.data().keySet()).containsExactlyInAnyOrder("my-secret"); + + Map data = result.data().get("my-secret"); + Assertions.assertThat(data.get("key1")).isEqualTo("value1"); + + } + + /** + *
+	 * 		- secrets 'my-secret' and 'my-secret-2' are deployed with label {color:pink}
+	 * 		- we search for them by same label and find them.
+	 * 
+ */ + @Test + void testSecretDataByLabelsTwoSecretsFound() { + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("my-secret").withLabels(Map.of("color", "pink")).build()) + .addToData(Map.of("property", Base64.getEncoder().encodeToString("value".getBytes()))) + .build()) + .create(); + + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata( + new ObjectMetaBuilder().withName("my-secret-2").withLabels(Map.of("color", "pink")).build()) + .addToData(Map.of("property-2", Base64.getEncoder().encodeToString("value-2".getBytes()))) + .build()) + .create(); + + MultipleSourcesContainer result = Fabric8ConfigUtils.secretsByLabels(client, "spring-k8s", + Map.of("color", "pink"), new MockEnvironment(), ReadType.SINGLE); + Assertions.assertThat(result.data().keySet()).contains("my-secret"); + Assertions.assertThat(result.data().keySet()).contains("my-secret-2"); + + Assertions.assertThat(result.data()).hasSize(2); + + Map secretData = result.data().get("my-secret"); + Assertions.assertThat(secretData.get("property")).isEqualTo("value"); + + Map secretData2 = result.data().get("my-secret-2"); + Assertions.assertThat(secretData2.get("property-2")).isEqualTo("value-2"); + + } + + /** + *
+	 *     - secret deployed with name "blue-circle-secret" and labels "color=blue, shape=circle, tag=fit"
+	 *     - secret deployed with name "blue-square-secret" and labels "color=blue, shape=square, tag=fit"
+	 *     - secret deployed with name "blue-triangle-secret" and labels "color=blue, shape=triangle, tag=no-fit"
+	 *     - secret deployed with name "blue-square-secret-k8s" and labels "color=blue, shape=triangle, tag=no-fit"
+	 *
+	 *     - we search by labels "color=blue, tag=fits", as such find two secrets: "blue-circle-secret"
+	 *       and "blue-square-secret".
+	 * 
+ */ + @Test + void testSecretDataByLabelsThreeSecretsFound() { + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("blue-circle-secret") + .withLabels(Map.of("color", "blue", "shape", "circle", "tag", "fit")) + .build()) + .addToData(Map.of("one", Base64.getEncoder().encodeToString("1".getBytes()))) + .build()) + .create(); + + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("blue-square-secret") + .withLabels(Map.of("color", "blue", "shape", "square", "tag", "fit")) + .build()) + .addToData(Map.of("two", Base64.getEncoder().encodeToString("2".getBytes()))) + .build()) + .create(); + + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("blue-triangle-secret") + .withLabels(Map.of("color", "blue", "shape", "triangle", "tag", "no-fit")) + .build()) + .addToData(Map.of("three", Base64.getEncoder().encodeToString("3".getBytes()))) + .build()) + .create(); + + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder() + .withMetadata(new ObjectMetaBuilder().withName("blue-square-secret-k8s") + .withLabels(Map.of("color", "blue", "shape", "triangle", "tag", "no-fit")) + .build()) + .addToData(Map.of("four", Base64.getEncoder().encodeToString("4".getBytes()))) + .build()) + .create(); + + MultipleSourcesContainer result = Fabric8ConfigUtils.secretsByLabels(client, "spring-k8s", + Map.of("tag", "fit", "color", "blue"), new MockEnvironment(), ReadType.SINGLE); + + Assertions.assertThat(result.data().keySet()).contains("blue-circle-secret"); + Assertions.assertThat(result.data().keySet()).contains("blue-square-secret"); + + Map data = result.data().get("blue-circle-secret"); + Assertions.assertThat(data.get("one")).isEqualTo("1"); + + Map data2 = result.data().get("blue-square-secret"); + Assertions.assertThat(data2.get("two")).isEqualTo("2"); + + } + + /** + *
+	 * 		- secret 'my-secret' is deployed; we search for it by name and do not find it.
+	 * 
+ */ + @Test + void testSecretDataByNameSecretNotFound() { + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder().withMetadata(new ObjectMetaBuilder().withName("my-secret").build()).build()) + .create(); + LinkedHashSet names = new LinkedHashSet<>(); + names.add("nope"); + MultipleSourcesContainer result = Fabric8ConfigUtils.secretsByName(client, "spring-k8s", names, + new MockEnvironment(), ReadType.SINGLE); + Assertions.assertThat(result.data()).isEmpty(); + } + + /** + *
+	 * 		- secret "my-secret" is deployed; we search for it by name and find it.
+	 * 
+ */ + @Test + void testSecretDataByNameSecretFound() { + client.secrets() + .inNamespace("spring-k8s") + .resource(new SecretBuilder().withMetadata(new ObjectMetaBuilder().withName("my-secret").build()) + .addToData(Map.of("property", Base64.getEncoder().encodeToString("value".getBytes()))) + .build()) + .create(); + LinkedHashSet names = new LinkedHashSet<>(); + names.add("my-secret"); + + MultipleSourcesContainer result = Fabric8ConfigUtils.secretsByName(client, "spring-k8s", names, + new MockEnvironment(), ReadType.SINGLE); + Assertions.assertThat(result.data().keySet()).hasSize(1); + + Map data = result.data().get("my-secret"); + Assertions.assertThat(data.get("property")).isEqualTo("value"); + } + + /** + *
+	 * 		- config-map "my-config-map" is deployed without any data
+	 * 		- we search for it by name and find it; but it has no data.
+	 * 
+ */ + @Test + void testConfigMapsDataByNameFoundNo() { + 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.configMapsByName(client, "spring-k8s", names, + new MockEnvironment(), ReadType.SINGLE); + Assertions.assertThat(result.data().keySet()).containsExactlyInAnyOrder("my-config-map"); + + Map data = result.data().get("my-config-map"); + Assertions.assertThat(data).isEmpty(); + } + + /** + *
+	 *     	- config-map "my-config-map" is deployed; we search for it and do not find it.
+	 * 
+ */ + @Test + void testConfigMapsByNameNotFound() { + 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.configMapsByName(client, "spring-k8s", names, + new MockEnvironment(), ReadType.SINGLE); + Assertions.assertThat(result.data()).isEmpty(); + } + + /** + *
+	 *     - config-map "my-config-map" is deployed; we search for it and find it
+	 * 
+ */ + @Test + void testConfigMapDataByNameFound() { + client.configMaps() + .inNamespace("spring-k8s") + .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) + .addToData(Map.of("property", "value")) + .build()) + .create(); + + LinkedHashSet names = new LinkedHashSet<>(); + names.add("my-config-map"); + + MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsByName(client, "spring-k8s", names, + new MockEnvironment(), ReadType.SINGLE); + Assertions.assertThat(result.data().keySet()).containsExactlyInAnyOrder("my-config-map"); + + Map data = result.data().get("my-config-map"); + Assertions.assertThat(data.get("property")).isEqualTo("value"); + + } + + /** + *
+	 *     - config-map "my-config-map" is deployed
+	 *     - we search for it and find it
+	 *     - it contains a single .yaml property, as such it gets some special treatment
+	 * 
+ */ + @Test + void testConfigMapDataByNameFoundWithPropertyFile() { + client.configMaps() + .inNamespace("spring-k8s") + .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) + .addToData(Map.of(APPLICATION_YAML, "key1: value1")) + .build()) + .create(); + + LinkedHashSet names = new LinkedHashSet<>(); + names.add("my-config-map"); + + MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsByName(client, "spring-k8s", names, + new MockEnvironment(), ReadType.SINGLE); + Assertions.assertThat(result.data().keySet()).containsExactlyInAnyOrder("my-config-map"); + + Map data = result.data().get("my-config-map"); + Assertions.assertThat(data.get("key1")).isEqualTo("value1"); + + } + + /** + *
+	 *     - config-map "my-config-map" and "my-config-map-2" are deployed
+	 *     - we search and find them.
+	 * 
+ */ + @Test + void testConfigMapDataByNameTwoFound() { + client.configMaps() + .inNamespace("spring-k8s") + .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) + .addToData(Map.of("property", "value")) + .build()) + .create(); + + client.configMaps() + .inNamespace("spring-k8s") + .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map-2").build()) + .addToData(Map.of("property-2", "value-2")) + .build()) + .create(); + + LinkedHashSet names = new LinkedHashSet<>(); + names.add("my-config-map"); + names.add("my-config-map-2"); + + MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsByName(client, "spring-k8s", names, + new MockEnvironment(), ReadType.SINGLE); + Assertions.assertThat(result.data().keySet()).containsExactlyInAnyOrder("my-config-map", "my-config-map-2"); + + Assertions.assertThat(result.data()).hasSize(2); + + Map data = result.data().get("my-config-map"); + Assertions.assertThat(data.get("property")).isEqualTo("value"); + + Map data2 = result.data().get("my-config-map-2"); + Assertions.assertThat(data2.get("property-2")).isEqualTo("value-2"); + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigUtilsTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigUtilsTests.java index 9eacc3a238..bd0c09fd39 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 @@ -17,383 +17,22 @@ package org.springframework.cloud.kubernetes.fabric8.config; import java.time.Duration; -import java.util.Base64; -import java.util.LinkedHashSet; -import java.util.Map; import java.util.Set; -import io.fabric8.kubernetes.api.model.ConfigMapBuilder; -import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.fabric8.kubernetes.api.model.SecretBuilder; -import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; -import org.springframework.cloud.kubernetes.commons.config.MultipleSourcesContainer; import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; import org.springframework.mock.env.MockEnvironment; -import static org.springframework.cloud.kubernetes.commons.config.Constants.APPLICATION_YAML; - /** * @author wind57 */ @EnableKubernetesMockClient(crud = true, https = false) class Fabric8ConfigUtilsTests { - private KubernetesClient client; - - @AfterEach - void afterEach() { - new Fabric8ConfigMapsCache().discardAll(); - new Fabric8SecretsCache().discardAll(); - } - - // secret "my-secret" is deployed without any labels; we search for it by labels - // "color=red" and do not find it. - @Test - void testSecretDataByLabelsSecretNotFound() { - client.secrets() - .inNamespace("spring-k8s") - .resource(new SecretBuilder().withMetadata(new ObjectMetaBuilder().withName("my-secret").build()).build()) - .create(); - MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByLabels(client, "spring-k8s", - Map.of("color", "red"), new MockEnvironment()); - Assertions.assertThat(result.data()).isEmpty(); - Assertions.assertThat(result.data().keySet()).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()); - Assertions.assertThat(result.data().keySet()).containsExactlyInAnyOrder("my-secret"); - - Map data = result.data().get("my-secret"); - Assertions.assertThat(data).containsExactlyInAnyOrderEntriesOf(Map.of("property", "value")); - } - - // secret "my-secret" is deployed with label {color:pink}; we search for it by same - // label and find it. This secret contains a single .yaml property, as such - // it gets some special treatment. - @Test - void testSecretDataByLabelsSecretFoundWithPropertyFile() { - client.secrets() - .inNamespace("spring-k8s") - .resource(new SecretBuilder() - .withMetadata(new ObjectMetaBuilder().withName("my-secret").withLabels(Map.of("color", "pink")).build()) - .addToData(Map.of(APPLICATION_YAML, Base64.getEncoder().encodeToString("key1: value1".getBytes()))) - .build()) - .create(); - - MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByLabels(client, "spring-k8s", - Map.of("color", "pink"), new MockEnvironment()); - Assertions.assertThat(result.data().keySet()).containsExactlyInAnyOrder("my-secret"); - - Map data = result.data().get("my-secret"); - Assertions.assertThat(data).containsExactlyInAnyOrderEntriesOf(Map.of("key1", "value1")); - } - - // secrets "my-secret" and "my-secret-2" are deployed with label {color:pink}; - // we search for them by same label and find them. - @Test - void testSecretDataByLabelsTwoSecretsFound() { - client.secrets() - .inNamespace("spring-k8s") - .resource(new SecretBuilder() - .withMetadata(new ObjectMetaBuilder().withName("my-secret").withLabels(Map.of("color", "pink")).build()) - .addToData(Map.of("property", Base64.getEncoder().encodeToString("value".getBytes()))) - .build()) - .create(); - - client.secrets() - .inNamespace("spring-k8s") - .resource(new SecretBuilder() - .withMetadata( - new ObjectMetaBuilder().withName("my-secret-2").withLabels(Map.of("color", "pink")).build()) - .addToData(Map.of("property-2", Base64.getEncoder().encodeToString("value-2".getBytes()))) - .build()) - .create(); - - MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByLabels(client, "spring-k8s", - Map.of("color", "pink"), new MockEnvironment()); - Assertions.assertThat(result.data().keySet()).contains("my-secret"); - Assertions.assertThat(result.data().keySet()).contains("my-secret-2"); - - Map mySecretData = result.data().get("my-secret"); - Assertions.assertThat(mySecretData).containsExactlyInAnyOrderEntriesOf(Map.of("property", "value")); - - Map mySecret2Data = result.data().get("my-secret-2"); - Assertions.assertThat(mySecret2Data).containsExactlyInAnyOrderEntriesOf(Map.of("property-2", "value-2")); - } - - /** - *
-	 *     - secret deployed with name "blue-circle-secret" and labels "color=blue, shape=circle, tag=fit"
-	 *     - secret deployed with name "blue-square-secret" and labels "color=blue, shape=square, tag=fit"
-	 *     - secret deployed with name "blue-triangle-secret" and labels "color=blue, shape=triangle, tag=no-fit"
-	 *     - secret deployed with name "blue-square-secret-k8s" and labels "color=blue, shape=triangle, tag=no-fit"
-	 *
-	 *     - we search by labels "color=blue, tag=fits", as such first find two secrets: "blue-circle-secret"
-	 *       and "blue-square-secret".
-	 *     - since "k8s" profile is enabled, we also take "blue-square-secret-k8s". Notice that this one does not match
-	 *       the initial labels (it has "tag=no-fit"), but it does not matter, we take it anyway.
-	 * 
- */ - @Test - void testSecretDataByLabelsThreeSecretsFound() { - client.secrets() - .inNamespace("spring-k8s") - .resource(new SecretBuilder() - .withMetadata(new ObjectMetaBuilder().withName("blue-circle-secret") - .withLabels(Map.of("color", "blue", "shape", "circle", "tag", "fit")) - .build()) - .addToData(Map.of("one", Base64.getEncoder().encodeToString("1".getBytes()))) - .build()) - .create(); - - client.secrets() - .inNamespace("spring-k8s") - .resource(new SecretBuilder() - .withMetadata(new ObjectMetaBuilder().withName("blue-square-secret") - .withLabels(Map.of("color", "blue", "shape", "square", "tag", "fit")) - .build()) - .addToData(Map.of("two", Base64.getEncoder().encodeToString("2".getBytes()))) - .build()) - .create(); - - client.secrets() - .inNamespace("spring-k8s") - .resource(new SecretBuilder() - .withMetadata(new ObjectMetaBuilder().withName("blue-triangle-secret") - .withLabels(Map.of("color", "blue", "shape", "triangle", "tag", "no-fit")) - .build()) - .addToData(Map.of("three", Base64.getEncoder().encodeToString("3".getBytes()))) - .build()) - .create(); - - client.secrets() - .inNamespace("spring-k8s") - .resource(new SecretBuilder() - .withMetadata(new ObjectMetaBuilder().withName("blue-square-secret-k8s") - .withLabels(Map.of("color", "blue", "shape", "triangle", "tag", "no-fit")) - .build()) - .addToData(Map.of("four", Base64.getEncoder().encodeToString("4".getBytes()))) - .build()) - .create(); - - MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByLabels(client, "spring-k8s", - Map.of("tag", "fit", "color", "blue"), new MockEnvironment()); - - Assertions.assertThat(result.data().keySet()) - .containsExactlyInAnyOrder("blue-circle-secret", "blue-square-secret"); - - Map dataBlueSecret = result.data().get("blue-circle-secret"); - Assertions.assertThat(dataBlueSecret).containsExactlyInAnyOrderEntriesOf(Map.of("one", "1")); - - Map dataSquareSecret = result.data().get("blue-square-secret"); - Assertions.assertThat(dataSquareSecret).containsExactlyInAnyOrderEntriesOf(Map.of("two", "2")); - - } - - // secret "my-secret" is deployed; we search for it by name and do not find it. - @Test - void testSecretDataByNameSecretNotFound() { - client.secrets() - .inNamespace("spring-k8s") - .resource(new SecretBuilder().withMetadata(new ObjectMetaBuilder().withName("my-secret").build()).build()) - .create(); - LinkedHashSet names = new LinkedHashSet<>(); - names.add("nope"); - MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByName(client, "spring-k8s", names, - new MockEnvironment()); - Assertions.assertThat(result.data()).isEmpty(); - Assertions.assertThat(result.data()).isEmpty(); - } - - // secret "my-secret" is deployed; we search for it by name and find it. - @Test - void testSecretDataByNameSecretFound() { - client.secrets() - .inNamespace("spring-k8s") - .resource(new SecretBuilder().withMetadata(new ObjectMetaBuilder().withName("my-secret").build()) - .addToData(Map.of("property", Base64.getEncoder().encodeToString("value".getBytes()))) - .build()) - .create(); - LinkedHashSet names = new LinkedHashSet<>(); - names.add("my-secret"); - - MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByName(client, "spring-k8s", names, - new MockEnvironment()); - Assertions.assertThat(result.data().size()).isEqualTo(1); - - Map data = result.data().get("my-secret"); - Assertions.assertThat(data.get("property")).isEqualTo("value"); - } - - // secrets "my-secret" and "my-secret-2" are deployed; - // we search for them by name label and find them. - @Test - void testSecretDataByNameTwoSecretsFound() { - client.secrets() - .inNamespace("spring-k8s") - .resource(new SecretBuilder().withMetadata(new ObjectMetaBuilder().withName("my-secret").build()) - .addToData(Map.of("property", Base64.getEncoder().encodeToString("value".getBytes()))) - .build()) - .create(); - - client.secrets() - .inNamespace("spring-k8s") - .resource(new SecretBuilder().withMetadata(new ObjectMetaBuilder().withName("my-secret-2").build()) - .addToData(Map.of("property-2", Base64.getEncoder().encodeToString("value-2".getBytes()))) - .build()) - .create(); - LinkedHashSet names = new LinkedHashSet<>(); - names.add("my-secret"); - names.add("my-secret-2"); - - MultipleSourcesContainer result = Fabric8ConfigUtils.secretsDataByName(client, "spring-k8s", names, - new MockEnvironment()); - Assertions.assertThat(result.data().keySet()).contains("my-secret"); - Assertions.assertThat(result.data().keySet()).contains("my-secret-2"); - - Assertions.assertThat(result.data().size()).isEqualTo(2); - - Map data = result.data().get("my-secret"); - Assertions.assertThat(data.get("property")).isEqualTo("value"); - - Map data2 = result.data().get("my-secret-2"); - Assertions.assertThat(data2.get("property-2")).isEqualTo("value-2"); - } - - // config-map "my-config-map" is deployed without any data; we search for it by name - // and find it; but it has no data. - @Test - void testConfigMapsDataByNameFoundNoData() { - client.configMaps() - .inNamespace("spring-k8s") - .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) - .build()) - .create(); - LinkedHashSet names = new LinkedHashSet<>(); - names.add("my-config-map"); - - MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsDataByName(client, "spring-k8s", names, - new MockEnvironment()); - Assertions.assertThat(result.data().keySet()).containsExactlyInAnyOrder("my-config-map"); - - Map data = result.data().get("my-config-map"); - Assertions.assertThat(data).isEmpty(); - } - - // config-map "my-config-map" is deployed; we search for it and do not find it. - @Test - void testConfigMapsDataByNameNotFound() { - client.configMaps() - .inNamespace("spring-k8s") - .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) - .build()) - .create(); - LinkedHashSet names = new LinkedHashSet<>(); - names.add("my-config-map-not-found"); - MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsDataByName(client, "spring-k8s", names, - new MockEnvironment()); - Assertions.assertThat(result.data().keySet()).isEmpty(); - Assertions.assertThat(result.data()).isEmpty(); - } - - // config-map "my-config-map" is deployed; we search for it and find it - @Test - void testConfigMapDataByNameFound() { - client.configMaps() - .inNamespace("spring-k8s") - .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) - .addToData(Map.of("property", "value")) - .build()) - .create(); - - LinkedHashSet names = new LinkedHashSet<>(); - names.add("my-config-map"); - - MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsDataByName(client, "spring-k8s", names, - new MockEnvironment()); - Assertions.assertThat(result.data().keySet()).containsExactlyInAnyOrder("my-config-map"); - - Map data = result.data().get("my-config-map"); - Assertions.assertThat(data).containsExactlyInAnyOrderEntriesOf(Map.of("property", "value")); - } - - // config-map "my-config-map" is deployed; we search for it and find it. - // It contains a single .yaml property, as such it gets some special treatment. - @Test - void testConfigMapDataByNameFoundWithPropertyFile() { - client.configMaps() - .inNamespace("spring-k8s") - .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) - .addToData(Map.of(APPLICATION_YAML, "key1: value1")) - .build()) - .create(); - - LinkedHashSet names = new LinkedHashSet<>(); - names.add("my-config-map"); - - MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsDataByName(client, "spring-k8s", names, - new MockEnvironment()); - Assertions.assertThat(result.data().keySet()).containsExactlyInAnyOrder("my-config-map"); - - Map data = result.data().get("my-config-map"); - Assertions.assertThat(data).containsExactlyInAnyOrderEntriesOf(Map.of("key1", "value1")); - } - - // config-map "my-config-map" and "my-config-map-2" are deployed; - // we search and find them. - @Test - void testConfigMapDataByNameTwoFound() { - client.configMaps() - .inNamespace("spring-k8s") - .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map").build()) - .addToData(Map.of("property", "value")) - .build()) - .create(); - - client.configMaps() - .inNamespace("spring-k8s") - .resource(new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder().withName("my-config-map-2").build()) - .addToData(Map.of("property-2", "value-2")) - .build()) - .create(); - - LinkedHashSet names = new LinkedHashSet<>(); - names.add("my-config-map"); - names.add("my-config-map-2"); - - MultipleSourcesContainer result = Fabric8ConfigUtils.configMapsDataByName(client, "spring-k8s", names, - new MockEnvironment()); - Assertions.assertThat(result.data().keySet()).contains("my-config-map"); - Assertions.assertThat(result.data().keySet()).contains("my-config-map-2"); - - Assertions.assertThat(result.data().size()).isEqualTo(2); - - Map data = result.data().get("my-config-map"); - Assertions.assertThat(data).containsExactlyInAnyOrderEntriesOf(Map.of("property", "value")); - - Map data2 = result.data().get("my-config-map-2"); - Assertions.assertThat(data2).containsExactlyInAnyOrderEntriesOf(Map.of("property-2", "value-2")); - } - @Test void testNamespacesFromProperties() { ConfigReloadProperties configReloadProperties = new ConfigReloadProperties(false, true, false, diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretErrorOnReadingSourceTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretErrorOnReadingSourceTests.java index 140eb630ac..3add57b598 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretErrorOnReadingSourceTests.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretErrorOnReadingSourceTests.java @@ -32,6 +32,7 @@ import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.cloud.kubernetes.commons.config.RetryProperties; import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties; import org.springframework.core.env.CompositePropertySource; @@ -71,7 +72,7 @@ void namedSingleSecretFails(CapturedOutput output) { mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").once(); SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), - List.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT); + List.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT, ReadType.BATCH); Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient, secretsConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -105,7 +106,8 @@ void namedTwoSecretsOneFails(CapturedOutput output) { Source sourceTwo = new Source(secretNameTwo, namespace, Map.of(), null, null, null); SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), - List.of(sourceOne, sourceTwo), true, null, namespace, false, true, false, RetryProperties.DEFAULT); + List.of(sourceOne, sourceTwo), true, null, namespace, false, true, false, RetryProperties.DEFAULT, + ReadType.BATCH); Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient, secretsConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -140,7 +142,8 @@ void namedTwoSecretsBothFail(CapturedOutput output) { Source sourceTwo = new Source(secretNameTwo, namespace, Map.of(), null, null, null); SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), - List.of(sourceOne, sourceTwo), true, null, namespace, false, true, false, RetryProperties.DEFAULT); + List.of(sourceOne, sourceTwo), true, null, namespace, false, true, false, RetryProperties.DEFAULT, + ReadType.BATCH); Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient, secretsConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -171,7 +174,8 @@ void labeledSingleSecretFails(CapturedOutput output) { Source secretSource = new Source(null, namespace, labels, null, null, null); SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, labels, List.of(), - List.of(secretSource), true, null, namespace, false, true, false, RetryProperties.DEFAULT); + List.of(secretSource), true, null, namespace, false, true, false, RetryProperties.DEFAULT, + ReadType.BATCH); Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient, secretsConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -216,7 +220,7 @@ void labeledTwoSecretsOneFails(CapturedOutput output) { SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of("one", "1", "two", "2"), List.of(), List.of(sourceOne, sourceTwo), true, null, namespace, false, - true, false, RetryProperties.DEFAULT); + true, false, RetryProperties.DEFAULT, ReadType.BATCH); Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient, secretsConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -256,7 +260,7 @@ void labeledTwoConfigMapsBothFail(CapturedOutput output) { SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of("one", "1", "two", "2"), List.of(), List.of(sourceOne, sourceTwo), true, null, namespace, false, - true, false, RetryProperties.DEFAULT); + true, false, RetryProperties.DEFAULT, ReadType.BATCH); Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient, secretsConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceLocatorTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8SecretsPropertySourceLocatorTests.java index 3e05178034..74f82a498c 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 @@ -30,6 +30,7 @@ import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.cloud.kubernetes.commons.config.RetryProperties; import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties; import org.springframework.core.env.CompositePropertySource; @@ -65,7 +66,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, ReadType.BATCH); Fabric8SecretsPropertySourceLocator locator = new Fabric8SecretsPropertySourceLocator(mockClient, configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment())); @@ -83,7 +84,7 @@ void locateShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled(CapturedOutput mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").always(); SecretsConfigProperties configMapConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), - List.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT); + List.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT, ReadType.BATCH); 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 8036cd0d5f..0c2c4da7f8 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 @@ -28,6 +28,7 @@ import org.springframework.cloud.kubernetes.commons.config.ConfigUtils; import org.springframework.cloud.kubernetes.commons.config.LabeledSecretNormalizedSource; import org.springframework.cloud.kubernetes.commons.config.NamedSecretNormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.mock.env.MockEnvironment; import static org.assertj.core.api.Assertions.assertThatNoException; @@ -57,7 +58,8 @@ 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(), + ReadType.BATCH); mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").always(); assertThatThrownBy(() -> new Fabric8SecretsPropertySource(context)).isInstanceOf(IllegalStateException.class) @@ -73,7 +75,8 @@ void labeledStrategyShouldThrowExceptionOnFailureWhenFailFastIsEnabled() { LabeledSecretNormalizedSource labeled = new LabeledSecretNormalizedSource(namespace, labels, true, ConfigUtils.Prefix.DEFAULT); - Fabric8ConfigContext context = new Fabric8ConfigContext(client, labeled, "default", new MockEnvironment()); + Fabric8ConfigContext context = new Fabric8ConfigContext(client, labeled, "default", new MockEnvironment(), + ReadType.BATCH); mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").always(); assertThatThrownBy(() -> new Fabric8SecretsPropertySource(context)).isInstanceOf(IllegalStateException.class) @@ -87,7 +90,8 @@ 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(), + ReadType.BATCH); mockServer.expect().withPath(path).andReturn(500, "Internal Server Error").always(); assertThatNoException().isThrownBy(() -> new Fabric8SecretsPropertySource(context)); @@ -101,7 +105,8 @@ void labeledStrategyShouldNotThrowExceptionOnFailureWhenFailFastIsDisabled() { LabeledSecretNormalizedSource labeled = new LabeledSecretNormalizedSource(namespace, labels, false, ConfigUtils.Prefix.DEFAULT); - Fabric8ConfigContext context = new Fabric8ConfigContext(client, labeled, "default", new MockEnvironment()); + Fabric8ConfigContext context = new Fabric8ConfigContext(client, labeled, "default", new MockEnvironment(), + ReadType.BATCH); 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/LabeledConfigMapContextToSourceDataProviderBatchReadTests.java similarity index 93% rename from spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledConfigMapContextToSourceDataProviderTests.java rename to spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledConfigMapContextToSourceDataProviderBatchReadTests.java index e476916691..c328cb69ef 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/LabeledConfigMapContextToSourceDataProviderBatchReadTests.java @@ -26,17 +26,16 @@ import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.test.system.CapturedOutput; -import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.config.ConfigUtils; import org.springframework.cloud.kubernetes.commons.config.LabeledConfigMapNormalizedSource; import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.cloud.kubernetes.commons.config.SourceData; import org.springframework.mock.env.MockEnvironment; @@ -46,8 +45,7 @@ * @author wind57 */ @EnableKubernetesMockClient(crud = true, https = false) -@ExtendWith(OutputCaptureExtension.class) -class LabeledConfigMapContextToSourceDataProviderTests { +class LabeledConfigMapContextToSourceDataProviderBatchReadTests { private static final String NAMESPACE = "default"; @@ -61,14 +59,14 @@ class LabeledConfigMapContextToSourceDataProviderTests { private static KubernetesClient mockClient; - static { - LABELS.put("label2", "value2"); - LABELS.put("label1", "value1"); - } + private static KubernetesMockServer mockServer; @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"); @@ -82,7 +80,7 @@ static void beforeAll() { @AfterEach void afterEach() { mockClient.configMaps().inNamespace(NAMESPACE).delete(); - new Fabric8ConfigMapsCache().discardAll(); + Fabric8SourcesBatchRead.discardConfigMaps(); } /** @@ -103,7 +101,7 @@ void singleConfigMapMatchAgainstLabels() { NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, LABELS, true, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), ReadType.BATCH); Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -147,7 +145,7 @@ void twoConfigMapsMatchAgainstLabels() { NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, RED_LABEL, true, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), ReadType.BATCH); Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -176,7 +174,7 @@ void configMapNoMatch() { NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, BLUE_LABEL, true, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), ReadType.BATCH); Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -207,7 +205,7 @@ void namespaceMatch() { NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE + "nope", LABELS, true, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), ReadType.BATCH); Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -237,7 +235,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(), ReadType.BATCH); Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -279,7 +277,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(), ReadType.BATCH); Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -328,7 +326,8 @@ void searchWithLabelsNoConfigmapsFound() { 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, + ReadType.BATCH); Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -361,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, + ReadType.BATCH); Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -405,7 +404,8 @@ void searchWithLabelsOneConfigMapFoundAndOneFromProfileFound() { NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DELAYED, true); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + ReadType.BATCH); Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -472,7 +472,8 @@ void searchWithLabelsTwoConfigMapsFound() { 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, + ReadType.BATCH); Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -481,6 +482,7 @@ void searchWithLabelsTwoConfigMapsFound() { assertThat(sourceData.sourceData().get("color-configmap.one")).isEqualTo("1"); assertThat(sourceData.sourceData().get("shape-configmap.two")).isEqualTo("2"); + assertThat(sourceData.sourceName()).isEqualTo("configmap.color-configmap.shape-configmap.default"); } @@ -494,7 +496,7 @@ void searchWithLabelsTwoConfigMapsFound() { * */ @Test - void cache(CapturedOutput output) { + void cache() { ConfigMap redConfigMap = new ConfigMapBuilder().withNewMetadata() .withName("red-configmap") .withLabels(Collections.singletonMap("color", "red")) @@ -517,32 +519,25 @@ 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, ReadType.BATCH); Fabric8ContextToSourceData redData = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData redSourceData = redData.apply(redContext); Assertions.assertThat(redSourceData.sourceData().size()).isEqualTo(1); - Assertions.assertThat(redSourceData.sourceData().get("red-configmap.one")).isEqualTo("1"); - Assertions.assertThat(output.getAll()).contains("Loaded all config maps in namespace '" + NAMESPACE + "'"); + + // delete the configmap, if caching is not present, the test would fail + mockClient.configMaps().inNamespace(NAMESPACE).withName(greenConfigmap.getMetadata().getName()).delete(); NormalizedSource greenNormalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, Collections.singletonMap("color", "green"), true, ConfigUtils.Prefix.DELAYED, true); Fabric8ConfigContext greenContext = new Fabric8ConfigContext(mockClient, greenNormalizedSource, NAMESPACE, - environment); + environment, ReadType.BATCH); Fabric8ContextToSourceData greenData = new LabeledConfigMapContextToSourceDataProvider().get(); SourceData greenSourceData = greenData.apply(greenContext); Assertions.assertThat(greenSourceData.sourceData().size()).isEqualTo(1); Assertions.assertThat(greenSourceData.sourceData().get("green-configmap.two")).isEqualTo("2"); - // meaning there is a single entry with such a log statement - String[] out = output.getAll().split("Loaded all config maps in namespace"); - Assertions.assertThat(out.length).isEqualTo(2); - - // meaning that the second read was done from the cache - out = output.getAll().split("Loaded \\(from cache\\) all config maps in namespace"); - Assertions.assertThat(out.length).isEqualTo(2); - } } diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledConfigMapContextToSourceDataProviderSingleReadTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledConfigMapContextToSourceDataProviderSingleReadTests.java new file mode 100644 index 0000000000..4417e2e0b0 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledConfigMapContextToSourceDataProviderSingleReadTests.java @@ -0,0 +1,504 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.config; + +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.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.ReadType; +import org.springframework.cloud.kubernetes.commons.config.SourceData; +import org.springframework.mock.env.MockEnvironment; + +/** + * @author wind57 + */ +@EnableKubernetesMockClient(crud = true, https = false) +class LabeledConfigMapContextToSourceDataProviderSingleReadTests { + + 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.configMaps().inNamespace(NAMESPACE).delete(); + Fabric8SourcesBatchRead.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(), ReadType.SINGLE); + + Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.test-configmap.default"); + Assertions.assertThat(sourceData.sourceData()).containsExactlyInAnyOrderEntriesOf(Map.of("name", "value")); + + } + + /** + * we have three configmaps deployed. two of them have labels that match (color=red), + * one does not (color=blue). + */ + @Test + void twoConfigMapsMatchAgainstLabels() { + + ConfigMap redOne = new ConfigMapBuilder().withNewMetadata() + .withName("red-configmap") + .withLabels(RED_LABEL) + .endMetadata() + .addToData("colorOne", "really-red") + .build(); + + ConfigMap redTwo = new ConfigMapBuilder().withNewMetadata() + .withName("red-configmap-again") + .withLabels(RED_LABEL) + .endMetadata() + .addToData("colorTwo", "really-red-again") + .build(); + + ConfigMap blue = new ConfigMapBuilder().withNewMetadata() + .withName("blue-configmap") + .withLabels(BLUE_LABEL) + .endMetadata() + .addToData("color", "blue") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(redOne).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(redTwo).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(blue).create(); + + NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, RED_LABEL, true, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), ReadType.SINGLE); + + Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red-configmap.red-configmap-again.default"); + Assertions.assertThat(sourceData.sourceData()).hasSize(2); + Assertions.assertThat(sourceData.sourceData().get("colorOne")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData().get("colorTwo")).isEqualTo("really-red-again"); + + } + + /** + * one configmap deployed (pink), does not match our query (blue). + */ + @Test + void configMapNoMatch() { + + ConfigMap pink = new ConfigMapBuilder().withNewMetadata() + .withName("pink-configmap") + .withLabels(PINK_LABEL) + .endMetadata() + .addToData("color", "pink") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(pink).create(); + + NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, BLUE_LABEL, true, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), ReadType.SINGLE); + + Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.color.default"); + Assertions.assertThat(sourceData.sourceData()).isEmpty(); + } + + /** + * LabeledConfigMapContextToSourceDataProvider gets as input a Fabric8ConfigContext. + * This context has a namespace as well as a NormalizedSource, that has a namespace + * too. It is easy to get confused in code on which namespace to use. This test makes + * sure that we use the proper one. + */ + @Test + void namespaceMatch() { + + ConfigMap configMap = new ConfigMapBuilder().withNewMetadata() + .withName("test-configmap") + .withLabels(LABELS) + .endMetadata() + .addToData("name", "value") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(configMap).create(); + + // different namespace + NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE + "nope", LABELS, true, + false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), ReadType.SINGLE); + + Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.test-configmap.default"); + Assertions.assertThat(sourceData.sourceData()).containsExactlyInAnyOrderEntriesOf(Map.of("name", "value")); + } + + /** + * one configmap with name : "blue-configmap" and labels "color=blue" is deployed. we + * search it with the same labels, find it, and assert that name of the SourceData (it + * must use its name, not its labels) and values in the SourceData must be prefixed + * (since we have provided an explicit prefix). + */ + @Test + void testWithPrefix() { + ConfigMap configMap = new ConfigMapBuilder().withNewMetadata() + .withName("blue-configmap") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("what-color", "blue-color") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(configMap).create(); + + ConfigUtils.Prefix mePrefix = ConfigUtils.findPrefix("me", false, false, "irrelevant"); + NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "blue"), true, mePrefix, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), ReadType.SINGLE); + + Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.blue-configmap.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("me.what-color", "blue-color")); + } + + /** + * two configmaps are deployed (name:blue-configmap, name:another-blue-configmap) and + * labels "color=blue" (on both). we search with the same labels, find them, and + * assert that name of the SourceData (it must use its name, not its labels) and + * values in the SourceData must be prefixed (since we have provided a delayed + * prefix). + * + * Also notice that the prefix is made up from both configmap names. + * + */ + @Test + void testTwoConfigmapsWithPrefix() { + ConfigMap blueConfigMap = new ConfigMapBuilder().withNewMetadata() + .withName("blue-configmap") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("first", "blue") + .build(); + + ConfigMap anotherBlue = new ConfigMapBuilder().withNewMetadata() + .withName("another-blue-configmap") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("second", "blue") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(blueConfigMap).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(anotherBlue).create(); + + NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DELAYED, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), ReadType.SINGLE); + + Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()) + .isEqualTo("configmap.another-blue-configmap.blue-configmap.default"); + + Map properties = sourceData.sourceData(); + Assertions.assertThat(properties).hasSize(2); + Iterator keys = properties.keySet().iterator(); + String firstKey = keys.next(); + String secondKey = keys.next(); + + if (firstKey.contains("first")) { + Assertions.assertThat(firstKey).isEqualTo("blue-configmap.first"); + } + + Assertions.assertThat(secondKey).isEqualTo("another-blue-configmap.second"); + Assertions.assertThat(properties.get(firstKey)).isEqualTo("blue"); + Assertions.assertThat(properties.get(secondKey)).isEqualTo("blue"); + } + + /** + * two configmaps are deployed: "color-configmap" with label: "{color:blue}" and + * "color-configmap-k8s" with no labels. We search by "{color:red}", do not find + * anything and thus have an empty SourceData. + */ + @Test + void searchWithLabelsNoConfigmapsFound() { + ConfigMap colorConfigmap = new ConfigMapBuilder().withNewMetadata() + .withName("color-configmap") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("one", "1") + .build(); + + ConfigMap colorConfigmapK8s = new ConfigMapBuilder().withNewMetadata() + .withName("color-configmap-k8s") + .endMetadata() + .addToData("two", "2") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(colorConfigmap).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(colorConfigmapK8s).create(); + MockEnvironment environment = new MockEnvironment(); + + NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "red"), true, ConfigUtils.Prefix.DEFAULT, true); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + ReadType.SINGLE); + + Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).isEmpty(); + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.color.default"); + + } + + /** + * two configmaps are deployed: "color-configmap" with label: "{color:blue}" and + * "shape-configmap" with label: "{shape:round}". We search by "{color:blue}" and find + * one configmap. + */ + @Test + void searchWithLabelsOneConfigMapFound() { + ConfigMap colorConfigmap = new ConfigMapBuilder().withNewMetadata() + .withName("color-configmap") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("one", "1") + .build(); + + ConfigMap shapeConfigmap = new ConfigMapBuilder().withNewMetadata() + .withName("shape-configmap") + .endMetadata() + .addToData("two", "2") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(colorConfigmap).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(shapeConfigmap).create(); + MockEnvironment environment = new MockEnvironment(); + + NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DEFAULT, true); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + ReadType.SINGLE); + + Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).hasSize(1); + Assertions.assertThat(sourceData.sourceData().get("one")).isEqualTo("1"); + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.color-configmap.default"); + + } + + /** + *
+	 *     - configmap "color-configmap" with label "{color:blue}"
+	 *     - configmap "shape-configmap" with labels "{color:blue, shape:round}"
+	 *     - configmap "no-fit" with labels "{tag:no-fit}"
+	 *     - configmap "color-configmap-k8s" with label "{color:red}"
+	 *     - configmap "shape-configmap-k8s" with label "{shape:triangle}"
+	 * 
+ */ + @Test + void searchWithLabelsTwoConfigMapsFound() { + ConfigMap colorConfigMap = new ConfigMapBuilder().withNewMetadata() + .withName("color-configmap") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("one", "1") + .build(); + + ConfigMap shapeConfigmap = new ConfigMapBuilder().withNewMetadata() + .withName("shape-configmap") + .withLabels(Map.of("color", "blue", "shape", "round")) + .endMetadata() + .addToData("two", "2") + .build(); + + ConfigMap noFit = new ConfigMapBuilder().withNewMetadata() + .withName("no-fit") + .withLabels(Map.of("tag", "no-fit")) + .endMetadata() + .addToData("three", "3") + .build(); + + ConfigMap colorConfigmapK8s = new ConfigMapBuilder().withNewMetadata() + .withName("color-configmap-k8s") + .withLabels(Map.of("color", "red")) + .endMetadata() + .addToData("four", "4") + .build(); + + ConfigMap shapeConfigmapK8s = new ConfigMapBuilder().withNewMetadata() + .withName("shape-configmap-k8s") + .withLabels(Map.of("shape", "triangle")) + .endMetadata() + .addToData("five", "5") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(colorConfigMap).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(shapeConfigmap).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(noFit).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(colorConfigmapK8s).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(shapeConfigmapK8s).create(); + + MockEnvironment environment = new MockEnvironment(); + + NormalizedSource normalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DELAYED, true); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + ReadType.SINGLE); + + Fabric8ContextToSourceData data = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).hasSize(2); + Assertions.assertThat(sourceData.sourceData().get("color-configmap.one")).isEqualTo("1"); + Assertions.assertThat(sourceData.sourceData().get("shape-configmap.two")).isEqualTo("2"); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.color-configmap.shape-configmap.default"); + + } + + /** + *
+	 *     - configmap "red-configmap" with label "{color:red}"
+	 *     - configmap "green-configmap" with labels "{color:green}"
+	 *     - we first search for "red" and find it, and it is retrieved from the cluster via the client.
+	 * 	   - we then search for the "green" one, and it is not retrieved from the cache.
+	 * 
+ */ + @Test + void nonCache() { + 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, ReadType.SINGLE); + Fabric8ContextToSourceData redData = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData redSourceData = redData.apply(redContext); + + Assertions.assertThat(redSourceData.sourceData()).hasSize(1); + Assertions.assertThat(redSourceData.sourceData().get("red-configmap.one")).isEqualTo("1"); + + NormalizedSource greenNormalizedSource = new LabeledConfigMapNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "green"), true, ConfigUtils.Prefix.DELAYED, true); + Fabric8ConfigContext greenContext = new Fabric8ConfigContext(mockClient, greenNormalizedSource, NAMESPACE, + environment, ReadType.SINGLE); + Fabric8ContextToSourceData greenData = new LabeledConfigMapContextToSourceDataProvider().get(); + SourceData greenSourceData = greenData.apply(greenContext); + + Assertions.assertThat(greenSourceData.sourceData()).hasSize(1); + Assertions.assertThat(greenSourceData.sourceData().get("green-configmap.two")).isEqualTo("2"); + + // since we do SINGLE reads, thus no caching is involved + // when we remove the configmap, nothing is found + mockClient.configMaps().inNamespace(NAMESPACE).resource(greenConfigmap).delete(); + + greenData = new LabeledConfigMapContextToSourceDataProvider().get(); + greenSourceData = greenData.apply(greenContext); + + Assertions.assertThat(greenSourceData.sourceData()).hasSize(0); + Assertions.assertThat(greenSourceData.sourceData().get("green-configmap.two")).isNull(); + + } + +} 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/LabeledSecretContextToSourceDataProviderBatchReadTests.java similarity index 92% rename from spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledSecretContextToSourceDataProviderTests.java rename to spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledSecretContextToSourceDataProviderBatchReadTests.java index 0b8118caed..4b55334fbf 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/LabeledSecretContextToSourceDataProviderBatchReadTests.java @@ -31,13 +31,11 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.test.system.CapturedOutput; -import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.config.ConfigUtils; import org.springframework.cloud.kubernetes.commons.config.LabeledSecretNormalizedSource; import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.cloud.kubernetes.commons.config.SourceData; import org.springframework.mock.env.MockEnvironment; @@ -49,8 +47,7 @@ * @author wind57 */ @EnableKubernetesMockClient(crud = true, https = false) -@ExtendWith(OutputCaptureExtension.class) -class LabeledSecretContextToSourceDataProviderTests { +class LabeledSecretContextToSourceDataProviderBatchReadTests { private static final String NAMESPACE = "default"; @@ -64,11 +61,6 @@ class LabeledSecretContextToSourceDataProviderTests { private static KubernetesClient mockClient; - static { - LABELS.put("label2", "value2"); - LABELS.put("label1", "value1"); - } - @BeforeAll static void beforeAll() { @@ -88,7 +80,7 @@ static void beforeAll() { @AfterEach void afterEach() { mockClient.secrets().inNamespace(NAMESPACE).delete(); - new Fabric8SecretsCache().discardAll(); + Fabric8SourcesBatchRead.discardSecrets(); } /** @@ -110,7 +102,7 @@ void singleSecretMatchAgainstLabels() { NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, LABELS, true, ConfigUtils.Prefix.DEFAULT); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), ReadType.BATCH); Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -156,7 +148,7 @@ void twoSecretsMatchAgainstLabels() { NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, RED_LABEL, true, ConfigUtils.Prefix.DEFAULT); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), ReadType.BATCH); Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -186,7 +178,7 @@ void secretNoMatch() { NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, BLUE_LABEL, true, ConfigUtils.Prefix.DEFAULT); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), ReadType.BATCH); Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -217,7 +209,7 @@ void namespaceMatch() { NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE + "nope", LABELS, true, ConfigUtils.Prefix.DEFAULT); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), ReadType.BATCH); Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -248,7 +240,7 @@ void testWithPrefix() { NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, Collections.singletonMap("color", "blue"), true, mePrefix); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), ReadType.BATCH); Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -289,7 +281,7 @@ void testTwoSecretsWithPrefix() { NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DELAYED); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), ReadType.BATCH); Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -314,8 +306,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() { @@ -335,11 +326,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); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + ReadType.BATCH); Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -352,7 +343,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() { @@ -372,11 +363,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); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + ReadType.BATCH); Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -416,7 +407,8 @@ void searchWithLabelsOneSecretFoundAndOneFromProfileFound() { NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DELAYED); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + ReadType.BATCH); Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -437,7 +429,7 @@ void searchWithLabelsOneSecretFoundAndOneFromProfileFound() { * */ @Test - void searchWithLabelsTwoSecretsFoundAndOneFromProfileFound() { + void searchWithLabelsTwoSecretsFound() { Secret colorSecret = new SecretBuilder().withNewMetadata() .withName("color-secret") .withLabels(Collections.singletonMap("color", "blue")) @@ -480,11 +472,11 @@ 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); - Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + ReadType.BATCH); Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -514,7 +506,7 @@ void testYaml() { NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DEFAULT); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), ReadType.BATCH); Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -533,7 +525,7 @@ void testYaml() { * */ @Test - void cache(CapturedOutput output) { + void cache() { Secret red = new SecretBuilder().withNewMetadata() .withName("red") .withLabels(Collections.singletonMap("color", "red")) @@ -556,32 +548,26 @@ void cache(CapturedOutput output) { NormalizedSource redNormalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, Collections.singletonMap("color", "red"), true, ConfigUtils.Prefix.DELAYED); Fabric8ConfigContext redContext = new Fabric8ConfigContext(mockClient, redNormalizedSource, NAMESPACE, - environment); + environment, ReadType.BATCH); Fabric8ContextToSourceData redData = new LabeledSecretContextToSourceDataProvider().get(); SourceData redSourceData = redData.apply(redContext); Assertions.assertThat(redSourceData.sourceData().size()).isEqualTo(1); Assertions.assertThat(redSourceData.sourceData().get("red.one")).isEqualTo("1"); - Assertions.assertThat(output.getAll()).contains("Loaded all secrets in namespace '" + NAMESPACE + "'"); + + // delete the configmap, if caching is not present, the test would fail + mockClient.secrets().inNamespace(NAMESPACE).withName(green.getMetadata().getName()).delete(); NormalizedSource greenNormalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, Collections.singletonMap("color", "green"), true, ConfigUtils.Prefix.DELAYED); Fabric8ConfigContext greenContext = new Fabric8ConfigContext(mockClient, greenNormalizedSource, NAMESPACE, - environment); + environment, ReadType.BATCH); Fabric8ContextToSourceData greenData = new LabeledSecretContextToSourceDataProvider().get(); SourceData greenSourceData = greenData.apply(greenContext); Assertions.assertThat(greenSourceData.sourceData().size()).isEqualTo(1); Assertions.assertThat(greenSourceData.sourceData().get("green.two")).isEqualTo("2"); - // meaning there is a single entry with such a log statement - String[] out = output.getAll().split("Loaded all secrets in namespace"); - Assertions.assertThat(out.length).isEqualTo(2); - - // meaning that the second read was done from the cache - out = output.getAll().split("Loaded \\(from cache\\) all secrets in namespace"); - Assertions.assertThat(out.length).isEqualTo(2); - } } diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledSecretContextToSourceDataProviderSingleReadTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledSecretContextToSourceDataProviderSingleReadTests.java new file mode 100644 index 0000000000..e89a466535 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/LabeledSecretContextToSourceDataProviderSingleReadTests.java @@ -0,0 +1,532 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.config; + +import java.util.Base64; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.kubernetes.commons.config.ConfigUtils; +import org.springframework.cloud.kubernetes.commons.config.LabeledSecretNormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.ReadType; +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 LabeledSecretContextToSourceDataProviderSingleReadTests { + + 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(); + Fabric8SourcesBatchRead.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, + ConfigUtils.Prefix.DEFAULT); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), ReadType.SINGLE); + + Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.test-secret.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("secretName", "secretValue")); + + } + + /** + * we have three secrets deployed. two of them have labels that match (color=red), one + * does not (color=blue). + */ + @Test + void twoSecretsMatchAgainstLabels() { + + Secret redOne = new SecretBuilder().withNewMetadata() + .withName("red-secret") + .withLabels(RED_LABEL) + .endMetadata() + .addToData("colorOne", Base64.getEncoder().encodeToString("really-red".getBytes())) + .build(); + + Secret redTwo = new SecretBuilder().withNewMetadata() + .withName("red-secret-again") + .withLabels(RED_LABEL) + .endMetadata() + .addToData("colorTwo", Base64.getEncoder().encodeToString("really-red-again".getBytes())) + .build(); + + Secret blue = new SecretBuilder().withNewMetadata() + .withName("blue-secret") + .withLabels(BLUE_LABEL) + .endMetadata() + .addToData("color", Base64.getEncoder().encodeToString("blue".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(redOne).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(redTwo).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(blue).create(); + + NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, RED_LABEL, true, + ConfigUtils.Prefix.DEFAULT); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), ReadType.SINGLE); + + Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red-secret.red-secret-again.default"); + Assertions.assertThat(sourceData.sourceData()).hasSize(2); + Assertions.assertThat(sourceData.sourceData().get("colorOne")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData().get("colorTwo")).isEqualTo("really-red-again"); + + } + + /** + * one secret deployed (pink), does not match our query (blue). + */ + @Test + void secretNoMatch() { + + Secret pink = new SecretBuilder().withNewMetadata() + .withName("pink-secret") + .withLabels(PINK_LABEL) + .endMetadata() + .addToData("color", Base64.getEncoder().encodeToString("pink".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(pink).create(); + + NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, BLUE_LABEL, true, + ConfigUtils.Prefix.DEFAULT); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), ReadType.SINGLE); + + Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.color.default"); + Assertions.assertThat(sourceData.sourceData()).isEmpty(); + } + + /** + * LabeledSecretContextToSourceDataProvider gets as input a Fabric8ConfigContext. This + * context has a namespace as well as a NormalizedSource, that has a namespace too. It + * is easy to get confused in code on which namespace to use. This test makes sure + * that we use the proper one. + */ + @Test + void namespaceMatch() { + + Secret secret = new SecretBuilder().withNewMetadata() + .withName("test-secret") + .withLabels(LABELS) + .endMetadata() + .addToData("secretName", Base64.getEncoder().encodeToString("secretValue".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(secret).create(); + + // different namespace + NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE + "nope", LABELS, true, + ConfigUtils.Prefix.DEFAULT); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), ReadType.SINGLE); + + Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.test-secret.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("secretName", "secretValue")); + } + + /** + * one secret with name : "blue-secret" and labels "color=blue" is deployed. we search + * it with the same labels, find it, and assert that name of the SourceData (it must + * use its name, not its labels) and values in the SourceData must be prefixed (since + * we have provided an explicit prefix). + */ + @Test + void testWithPrefix() { + Secret secret = new SecretBuilder().withNewMetadata() + .withName("blue-secret") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("what-color", Base64.getEncoder().encodeToString("blue-color".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(secret).create(); + + ConfigUtils.Prefix mePrefix = ConfigUtils.findPrefix("me", false, false, "irrelevant"); + NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "blue"), true, mePrefix); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), ReadType.SINGLE); + + Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.blue-secret.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("me.what-color", "blue-color")); + } + + /** + * two secrets are deployed (name:blue-secret, name:another-blue-secret) and labels + * "color=blue" (on both). we search with the same labels, find them, and assert that + * name of the SourceData (it must use its name, not its labels) and values in the + * SourceData must be prefixed (since we have provided a delayed prefix). + * + * Also notice that the prefix is made up from both secret names. + * + */ + @Test + void testTwoSecretsWithPrefix() { + Secret blueSecret = new SecretBuilder().withNewMetadata() + .withName("blue-secret") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("first", Base64.getEncoder().encodeToString("blue".getBytes())) + .build(); + + Secret anotherBlue = new SecretBuilder().withNewMetadata() + .withName("another-blue-secret") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("second", Base64.getEncoder().encodeToString("blue".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(blueSecret).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(anotherBlue).create(); + + NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DELAYED); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), ReadType.SINGLE); + + Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.another-blue-secret.blue-secret.default"); + + Map properties = sourceData.sourceData(); + Assertions.assertThat(properties).hasSize(2); + Iterator keys = properties.keySet().iterator(); + String firstKey = keys.next(); + String secondKey = keys.next(); + + if (firstKey.contains("first")) { + Assertions.assertThat(firstKey).isEqualTo("blue-secret.first"); + } + + Assertions.assertThat(secondKey).isEqualTo("another-blue-secret.second"); + Assertions.assertThat(properties.get(firstKey)).isEqualTo("blue"); + Assertions.assertThat(properties.get(secondKey)).isEqualTo("blue"); + } + + /** + * two secrets are deployed: secret "color-secret" with label: "{color:blue}" and + * "color-secret-k8s" with no labels. We search by "{color:red}", do not find anything + * and thus have an empty SourceData. + */ + @Test + void searchWithLabelsNoSecretFound() { + Secret colorSecret = new SecretBuilder().withNewMetadata() + .withName("color-secret") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("one", Base64.getEncoder().encodeToString("1".getBytes())) + .build(); + + Secret colorSecretK8s = new SecretBuilder().withNewMetadata() + .withName("color-secret-k8s") + .endMetadata() + .addToData("two", Base64.getEncoder().encodeToString("2".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(colorSecret).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(colorSecretK8s).create(); + MockEnvironment environment = new MockEnvironment(); + + NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "red"), true, ConfigUtils.Prefix.DEFAULT); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + ReadType.SINGLE); + + Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).isEmpty(); + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.color.default"); + + } + + /** + * two secrets are deployed: secret "color-secret" with label: "{color:blue}" and + * "shape-secret" with label: "{shape:round}". We search by "{color:blue}" and find + * one secret. + */ + @Test + void searchWithLabelsOneSecretFound() { + Secret colorSecret = new SecretBuilder().withNewMetadata() + .withName("color-secret") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("one", Base64.getEncoder().encodeToString("1".getBytes())) + .build(); + + Secret shapeSecret = new SecretBuilder().withNewMetadata() + .withName("shape-secret") + .endMetadata() + .addToData("two", Base64.getEncoder().encodeToString("2".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(colorSecret).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(shapeSecret).create(); + MockEnvironment environment = new MockEnvironment(); + + NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DEFAULT); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + ReadType.SINGLE); + + Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).hasSize(1); + Assertions.assertThat(sourceData.sourceData().get("one")).isEqualTo("1"); + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.color-secret.default"); + + } + + /** + *
+	 *     - secret "color-secret" with label "{color:blue}"
+	 *     - secret "shape-secret" with labels "{color:blue, shape:round}"
+	 *     - secret "no-fit" with labels "{tag:no-fit}"
+	 *     - secret "color-secret-k8s" with label "{color:red}"
+	 *     - secret "shape-secret-k8s" with label "{shape:triangle}"
+	 * 
+ */ + @Test + void searchWithLabelsTwoSecretsFound() { + Secret colorSecret = new SecretBuilder().withNewMetadata() + .withName("color-secret") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("one", Base64.getEncoder().encodeToString("1".getBytes())) + .build(); + + Secret shapeSecret = new SecretBuilder().withNewMetadata() + .withName("shape-secret") + .withLabels(Map.of("color", "blue", "shape", "round")) + .endMetadata() + .addToData("two", Base64.getEncoder().encodeToString("2".getBytes())) + .build(); + + Secret noFit = new SecretBuilder().withNewMetadata() + .withName("no-fit") + .withLabels(Map.of("tag", "no-fit")) + .endMetadata() + .addToData("three", Base64.getEncoder().encodeToString("3".getBytes())) + .build(); + + Secret colorSecretK8s = new SecretBuilder().withNewMetadata() + .withName("color-secret-k8s") + .withLabels(Map.of("color", "red")) + .endMetadata() + .addToData("four", Base64.getEncoder().encodeToString("4".getBytes())) + .build(); + + Secret shapeSecretK8s = new SecretBuilder().withNewMetadata() + .withName("shape-secret-k8s") + .withLabels(Map.of("shape", "triangle")) + .endMetadata() + .addToData("five", Base64.getEncoder().encodeToString("5".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(colorSecret).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(shapeSecret).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(noFit).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(colorSecretK8s).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(shapeSecretK8s).create(); + + MockEnvironment environment = new MockEnvironment(); + + NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DELAYED); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + ReadType.SINGLE); + + Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).hasSize(2); + Assertions.assertThat(sourceData.sourceData().get("color-secret.one")).isEqualTo("1"); + Assertions.assertThat(sourceData.sourceData().get("shape-secret.two")).isEqualTo("2"); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.color-secret.shape-secret.default"); + + } + + /** + * yaml/properties gets special treatment + */ + @Test + void testYaml() { + Secret colorSecret = new SecretBuilder().withNewMetadata() + .withName("color-secret") + .withLabels(Collections.singletonMap("color", "blue")) + .endMetadata() + .addToData("test.yaml", Base64.getEncoder().encodeToString("color: blue".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(colorSecret).create(); + + NormalizedSource normalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "blue"), true, ConfigUtils.Prefix.DEFAULT); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), ReadType.SINGLE); + + Fabric8ContextToSourceData data = new LabeledSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceData()).hasSize(1); + Assertions.assertThat(sourceData.sourceData().get("color")).isEqualTo("blue"); + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.color-secret.default"); + } + + /** + *
+	 *     - secret "red" with label "{color:red}"
+	 *     - secret "green" with labels "{color:green}"
+	 *     - we first search for "red" and find it, and it is retrieved from the cluster via the client.
+	 * 	   - we then search for the "green" one, and it is retrieved from the cache this time.
+	 * 
+ */ + @Test + void cache(CapturedOutput output) { + Secret red = new SecretBuilder().withNewMetadata() + .withName("red") + .withLabels(Collections.singletonMap("color", "red")) + .endMetadata() + .addToData("one", Base64.getEncoder().encodeToString("1".getBytes())) + .build(); + + Secret green = new SecretBuilder().withNewMetadata() + .withName("green") + .withLabels(Map.of("color", "green")) + .endMetadata() + .addToData("two", Base64.getEncoder().encodeToString("2".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(red).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(green).create(); + + MockEnvironment environment = new MockEnvironment(); + + NormalizedSource redNormalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "red"), true, ConfigUtils.Prefix.DELAYED); + Fabric8ConfigContext redContext = new Fabric8ConfigContext(mockClient, redNormalizedSource, NAMESPACE, + environment, ReadType.SINGLE); + Fabric8ContextToSourceData redData = new LabeledSecretContextToSourceDataProvider().get(); + SourceData redSourceData = redData.apply(redContext); + + Assertions.assertThat(redSourceData.sourceData()).hasSize(1); + Assertions.assertThat(redSourceData.sourceData().get("red.one")).isEqualTo("1"); + + Assertions.assertThat(output.getAll()).doesNotContain("Loaded all secrets in namespace '" + NAMESPACE + "'"); + Assertions.assertThat(output.getAll()).contains("Will read individual secrets in namespace"); + + NormalizedSource greenNormalizedSource = new LabeledSecretNormalizedSource(NAMESPACE, + Collections.singletonMap("color", "green"), true, ConfigUtils.Prefix.DELAYED); + Fabric8ConfigContext greenContext = new Fabric8ConfigContext(mockClient, greenNormalizedSource, NAMESPACE, + environment, ReadType.SINGLE); + Fabric8ContextToSourceData greenData = new LabeledSecretContextToSourceDataProvider().get(); + SourceData greenSourceData = greenData.apply(greenContext); + + Assertions.assertThat(greenSourceData.sourceData()).hasSize(1); + Assertions.assertThat(greenSourceData.sourceData().get("green.two")).isEqualTo("2"); + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/MissingActuatorTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/MissingActuatorTest.java deleted file mode 100644 index 3c9c00f22d..0000000000 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/MissingActuatorTest.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2013-present 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 org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.boot.test.system.CapturedOutput; -import org.springframework.boot.test.system.OutputCaptureExtension; -import org.springframework.cloud.test.ClassPathExclusions; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Configuration; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import static org.assertj.core.api.Assertions.assertThat; - -// inspired by spring-cloud-commons: RefreshAutoConfigurationMoreClassPathTests - -@ExtendWith({ SpringExtension.class, OutputCaptureExtension.class }) -@ClassPathExclusions({ "spring-boot-actuator-autoconfigure-*.jar", "spring-boot-starter-actuator-*.jar" }) -class MissingActuatorTest { - - private static ConfigurableApplicationContext getApplicationContext(String... properties) { - return new SpringApplicationBuilder(Config.class).web(WebApplicationType.NONE).properties(properties).run(); - } - - @Test - void unknownClassProtected(CapturedOutput capturedOutput) { - try (ConfigurableApplicationContext context = getApplicationContext("debug=true", - "spring.cloud.kubernetes.client.namespace=default")) { - String output = capturedOutput.toString(); - assertThat(output) - .doesNotContain("Failed to introspect annotations on" - + " [class org.springframework.cloud.autoconfigure.RefreshEndpointAutoConfiguration") - .doesNotContain("TypeNotPresentExceptionProxy"); - } - } - - @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration - static class Config { - - } - -} 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/NamedConfigMapContextToSourceDataProviderBatchReadTests.java similarity index 90% 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/NamedConfigMapContextToSourceDataProviderBatchReadTests.java index 7caa916f15..277c155620 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/NamedConfigMapContextToSourceDataProviderBatchReadTests.java @@ -27,13 +27,11 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.test.system.CapturedOutput; -import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.config.ConfigUtils; import org.springframework.cloud.kubernetes.commons.config.NamedConfigMapNormalizedSource; import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.cloud.kubernetes.commons.config.SourceData; import org.springframework.mock.env.MockEnvironment; @@ -43,8 +41,7 @@ * @author wind57 */ @EnableKubernetesMockClient(crud = true, https = false) -@ExtendWith(OutputCaptureExtension.class) -class NamedConfigMapContextToSourceDataProviderTests { +class NamedConfigMapContextToSourceDataProviderBatchReadTests { private static final String NAMESPACE = "default"; @@ -70,7 +67,7 @@ static void beforeAll() { @AfterEach void afterEach() { mockClient.configMaps().inNamespace(NAMESPACE).delete(); - new Fabric8ConfigMapsCache().discardAll(); + Fabric8SourcesBatchRead.discardConfigMaps(); } /** @@ -92,7 +89,7 @@ void noMatch() { NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("blue", NAMESPACE, true, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), ReadType.BATCH); Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -121,7 +118,7 @@ void match() { NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("red", NAMESPACE, true, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), ReadType.BATCH); Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -162,7 +159,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, + ReadType.BATCH); Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -175,12 +173,12 @@ void matchIncludeSingleProfile() { /** *
-	*     - 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.
-	* 
+ * - 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() { @@ -206,7 +204,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, + ReadType.BATCH); Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -255,7 +254,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, + ReadType.BATCH); Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -285,7 +285,7 @@ void matchWithName() { NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("application", NAMESPACE, true, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), ReadType.BATCH); Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -315,7 +315,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(), ReadType.BATCH); Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -342,7 +342,7 @@ void testSingleYaml() { NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("red", NAMESPACE, true, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), ReadType.BATCH); Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -372,7 +372,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, + ReadType.BATCH); Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -389,7 +390,7 @@ void testCorrectNameWithProfile() { * */ @Test - void cache(CapturedOutput output) { + void cache() { ConfigMap red = new ConfigMapBuilder().withNewMetadata() .withName("red") @@ -409,18 +410,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, + ReadType.BATCH); Fabric8ContextToSourceData redData = new NamedConfigMapContextToSourceDataProvider().get(); SourceData redSourceData = redData.apply(redContext); Assertions.assertThat(redSourceData.sourceName()).isEqualTo("configmap.red.default"); Assertions.assertThat(redSourceData.sourceData().size()).isEqualTo(1); Assertions.assertThat(redSourceData.sourceData().get("some.color")).isEqualTo("really-red"); - Assertions.assertThat(output.getAll()).contains("Loaded all config maps in namespace '" + NAMESPACE + "'"); + + // delete the configmap, if caching is not present, the test would fail + mockClient.configMaps().inNamespace(NAMESPACE).withName(green.getMetadata().getName()).delete(); 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, + ReadType.BATCH); Fabric8ContextToSourceData greenData = new NamedConfigMapContextToSourceDataProvider().get(); SourceData greenSourceData = greenData.apply(greenContext); @@ -428,14 +433,6 @@ void cache(CapturedOutput output) { Assertions.assertThat(greenSourceData.sourceData().size()).isEqualTo(1); Assertions.assertThat(greenSourceData.sourceData().get("some.taste")).isEqualTo("mango"); - // meaning there is a single entry with such a log statement - String[] out = output.getAll().split("Loaded all config maps in namespace"); - Assertions.assertThat(out.length).isEqualTo(2); - - // meaning that the second read was done from the cache - out = output.getAll().split("Loaded \\(from cache\\) all config maps in namespace"); - Assertions.assertThat(out.length).isEqualTo(2); - } } diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/NamedConfigMapContextToSourceDataProviderSingleBatchReadTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/NamedConfigMapContextToSourceDataProviderSingleBatchReadTests.java new file mode 100644 index 0000000000..5df5dfe961 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/NamedConfigMapContextToSourceDataProviderSingleBatchReadTests.java @@ -0,0 +1,451 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.config; + +import java.util.Collections; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.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.ReadType; +import org.springframework.cloud.kubernetes.commons.config.SourceData; +import org.springframework.mock.env.MockEnvironment; + +/** + * @author wind57 + */ +@EnableKubernetesMockClient(crud = true, https = false) +class NamedConfigMapContextToSourceDataProviderSingleBatchReadTests { + + 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(); + Fabric8SourcesBatchRead.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(), ReadType.SINGLE); + + Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.blue.default"); + Assertions.assertThat(sourceData.sourceData()).isEmpty(); + + } + + /** + *
+	 *     one configmap deployed with name "red"
+	 *     we search by name, for the "red" one, as such we find it
+	 * 
+ */ + @Test + void match() { + + ConfigMap configMap = new ConfigMapBuilder().withNewMetadata() + .withName("red") + .endMetadata() + .addToData(COLOR_REALLY_RED) + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(configMap).create(); + + NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("red", NAMESPACE, true, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), ReadType.SINGLE); + + Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red.default"); + Assertions.assertThat(sourceData.sourceData()).isEqualTo(COLOR_REALLY_RED); + + } + + /** + *
+	 *     - two configmaps deployed : "red" and "red-with-profile".
+	 *     - "red" is matched directly, "red-with-profile" is matched because we have an active profile
+	 *       "active-profile"
+	 * 
+ */ + @Test + void matchIncludeSingleProfile() { + + ConfigMap red = new ConfigMapBuilder().withNewMetadata() + .withName("red") + .endMetadata() + .addToData(COLOR_REALLY_RED) + .build(); + + ConfigMap redWithProfile = new ConfigMapBuilder().withNewMetadata() + .withName("red-with-profile") + .endMetadata() + .addToData("taste", "mango") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(red).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(redWithProfile).create(); + + // add one more profile and specify that we want profile based config maps + MockEnvironment env = new MockEnvironment(); + env.setActiveProfiles("with-profile"); + NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("red", NAMESPACE, true, + ConfigUtils.Prefix.DEFAULT, true, true); + + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env, + ReadType.SINGLE); + + Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red.red-with-profile.default.with-profile"); + Assertions.assertThat(sourceData.sourceData()).hasSize(2); + Assertions.assertThat(sourceData.sourceData().get("color")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData().get("taste")).isEqualTo("mango"); + + } + + /** + *
+	 *     - two configmaps deployed : "red" and "red-with-profile".
+	 *     - "red" is matched directly, "red-with-profile" is matched because we have an active profile
+	 *       "active-profile"
+	 *     -  This takes into consideration the prefix, that we explicitly specify.
+	 *        Notice that prefix works for profile based config maps as well.
+	 * 
+ */ + @Test + void matchIncludeSingleProfileWithPrefix() { + + ConfigMap red = new ConfigMapBuilder().withNewMetadata() + .withName("red") + .endMetadata() + .addToData(COLOR_REALLY_RED) + .build(); + + ConfigMap redWithProfile = new ConfigMapBuilder().withNewMetadata() + .withName("red-with-profile") + .endMetadata() + .addToData("taste", "mango") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(red).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(redWithProfile).create(); + + // add one more profile and specify that we want profile based config maps + // also append prefix + MockEnvironment env = new MockEnvironment(); + env.setActiveProfiles("with-profile"); + NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("red", NAMESPACE, true, PREFIX, true); + + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env, + ReadType.SINGLE); + + Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red.red-with-profile.default"); + Assertions.assertThat(sourceData.sourceData()).hasSize(2); + Assertions.assertThat(sourceData.sourceData().get("some.color")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData().get("some.taste")).isEqualTo("mango"); + + } + + /** + *
+	 *     - three configmaps deployed : "red", "red-with-taste" and "red-with-shape"
+	 *     - "red" is matched directly, the other two are matched because of active profiles
+	 *     -  This takes into consideration the prefix, that we explicitly specify.
+	 *        Notice that prefix works for profile based config maps as well.
+	 * 
+ */ + @Test + void matchIncludeTwoProfilesWithPrefix() { + + ConfigMap red = new ConfigMapBuilder().withNewMetadata() + .withName("red") + .endMetadata() + .addToData(COLOR_REALLY_RED) + .build(); + + ConfigMap redWithTaste = new ConfigMapBuilder().withNewMetadata() + .withName("red-with-taste") + .endMetadata() + .addToData("taste", "mango") + .build(); + + ConfigMap redWithShape = new ConfigMapBuilder().withNewMetadata() + .withName("red-with-shape") + .endMetadata() + .addToData("shape", "round") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(red).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(redWithTaste).create(); + mockClient.configMaps().inNamespace(NAMESPACE).resource(redWithShape).create(); + + // add one more profile and specify that we want profile based config maps + // also append prefix + MockEnvironment env = new MockEnvironment(); + env.setActiveProfiles("with-taste", "with-shape"); + NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("red", NAMESPACE, true, PREFIX, true); + + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env, + ReadType.SINGLE); + + Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red.red-with-shape.red-with-taste.default"); + Assertions.assertThat(sourceData.sourceData()).hasSize(3); + Assertions.assertThat(sourceData.sourceData().get("some.color")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData().get("some.taste")).isEqualTo("mango"); + Assertions.assertThat(sourceData.sourceData().get("some.shape")).isEqualTo("round"); + + } + + /** + *
+	 * 		proves that an implicit configmap is going to be generated and read, even if
+	 * 	    we did not provide one
+	 * 
+ */ + @Test + void matchWithName() { + ConfigMap configMap = new ConfigMapBuilder().withNewMetadata() + .withName("application") + .endMetadata() + .addToData("color", "red") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(configMap).create(); + + NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("application", NAMESPACE, true, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), ReadType.SINGLE); + + Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.application.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Collections.singletonMap("color", "red")); + } + + /** + *
+	 *     - NamedSecretContextToSourceDataProvider gets as input a KubernetesClientConfigContext
+	 *     - This context has a namespace as well as a NormalizedSource, that has a namespace too.
+	 *     - This test makes sure that we use the proper one.
+	 * 
+ */ + @Test + void namespaceMatch() { + + ConfigMap configMap = new ConfigMapBuilder().withNewMetadata() + .withName("red") + .endMetadata() + .addToData(COLOR_REALLY_RED) + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(configMap).create(); + + String wrongNamespace = NAMESPACE + "nope"; + NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("red", wrongNamespace, true, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), ReadType.SINGLE); + + Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Collections.singletonMap("color", "really-red")); + } + + /** + *
+	 *     - proves that single yaml file gets special treatment
+	 * 
+ */ + @Test + void testSingleYaml() { + ConfigMap configMap = new ConfigMapBuilder().withNewMetadata() + .withName("red") + .endMetadata() + .addToData("single.yaml", "key: value") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(configMap).create(); + + NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("red", NAMESPACE, true, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), ReadType.SINGLE); + + Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.red.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Collections.singletonMap("key", "value")); + } + + /** + *
+	 *     - one configmap is deployed with name "one"
+	 *     - profile is enabled with name "k8s"
+	 *
+	 *     we assert that the name of the source is "one" and does not contain "one-dev"
+	 * 
+ */ + @Test + void testCorrectNameWithProfile() { + ConfigMap configMap = new ConfigMapBuilder().withNewMetadata() + .withName("one") + .endMetadata() + .addToData("key", "value") + .build(); + + mockClient.configMaps().inNamespace(NAMESPACE).resource(configMap).create(); + MockEnvironment environment = new MockEnvironment(); + environment.setActiveProfiles("k8s"); + + NormalizedSource normalizedSource = new NamedConfigMapNormalizedSource("one", NAMESPACE, true, true); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, environment, + ReadType.SINGLE); + + Fabric8ContextToSourceData data = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("configmap.one.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Collections.singletonMap("key", "value")); + } + + /** + *
+	 *     - two configmaps are deployed : "red", "green", in the same namespace.
+	 *     - we first search for "red" and find it, and it is retrieved from the cluster via the client.
+	 *     - we then search for the "green" one and it is not retrieved from the cache.
+	 * 
+ */ + @Test + void nonCache() { + + 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, + ReadType.SINGLE); + Fabric8ContextToSourceData redData = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData redSourceData = redData.apply(redContext); + + Assertions.assertThat(redSourceData.sourceName()).isEqualTo("configmap.red.default"); + Assertions.assertThat(redSourceData.sourceData()).hasSize(1); + Assertions.assertThat(redSourceData.sourceData().get("some.color")).isEqualTo("really-red"); + + NormalizedSource greenNormalizedSource = new NamedConfigMapNormalizedSource("green", NAMESPACE, true, PREFIX, + false); + Fabric8ConfigContext greenContext = new Fabric8ConfigContext(mockClient, greenNormalizedSource, NAMESPACE, env, + ReadType.SINGLE); + Fabric8ContextToSourceData greenData = new NamedConfigMapContextToSourceDataProvider().get(); + SourceData greenSourceData = greenData.apply(greenContext); + + Assertions.assertThat(greenSourceData.sourceName()).isEqualTo("configmap.green.default"); + Assertions.assertThat(greenSourceData.sourceData()).hasSize(1); + Assertions.assertThat(greenSourceData.sourceData().get("some.taste")).isEqualTo("mango"); + + // since we do SINGLE reads, thus no caching is involved + // when we remove the configmap, nothing is found + mockClient.configMaps().inNamespace(NAMESPACE).resource(green).delete(); + + greenData = new NamedConfigMapContextToSourceDataProvider().get(); + greenSourceData = greenData.apply(greenContext); + + Assertions.assertThat(greenSourceData.sourceData()).hasSize(0); + Assertions.assertThat(greenSourceData.sourceData().get("some.taste")).isNull(); + + } + +} 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/NamedSecretContextToSourceDataProviderBatchReadTests.java similarity index 92% 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/NamedSecretContextToSourceDataProviderBatchReadTests.java index c7ae1dc569..6626796658 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/NamedSecretContextToSourceDataProviderBatchReadTests.java @@ -28,13 +28,11 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.test.system.CapturedOutput; -import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.config.ConfigUtils; import org.springframework.cloud.kubernetes.commons.config.NamedSecretNormalizedSource; import org.springframework.cloud.kubernetes.commons.config.NormalizedSource; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.cloud.kubernetes.commons.config.SourceData; import org.springframework.mock.env.MockEnvironment; @@ -44,8 +42,7 @@ * @author wind57 */ @EnableKubernetesMockClient(crud = true, https = false) -@ExtendWith(OutputCaptureExtension.class) -class NamedSecretContextToSourceDataProviderTests { +class NamedSecretContextToSourceDataProviderBatchReadTests { private static final String NAMESPACE = "default"; @@ -69,7 +66,7 @@ static void beforeAll() { @AfterEach void afterEach() { mockClient.secrets().inNamespace(NAMESPACE).delete(); - new Fabric8SecretsCache().discardAll(); + Fabric8SourcesBatchRead.discardSecrets(); } /** @@ -88,7 +85,7 @@ void singleSecretMatchAgainstLabels() { NormalizedSource normalizedSource = new NamedSecretNormalizedSource("red", NAMESPACE, true, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), ReadType.BATCH); Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -130,7 +127,7 @@ void twoSecretMatchAgainstLabels() { NormalizedSource normalizedSource = new NamedSecretNormalizedSource("red", NAMESPACE, true, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), ReadType.BATCH); Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -157,7 +154,7 @@ void testSecretNoMatch() { NormalizedSource normalizedSource = new NamedSecretNormalizedSource("blue", NAMESPACE, true, false); Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, - new MockEnvironment()); + new MockEnvironment(), ReadType.BATCH); Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -186,7 +183,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(), ReadType.BATCH); Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -223,7 +220,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, + ReadType.BATCH); Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -265,7 +263,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, + ReadType.BATCH); Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -314,7 +313,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, + ReadType.BATCH); Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -346,7 +346,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(), ReadType.BATCH); Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); SourceData sourceData = data.apply(context); @@ -363,7 +363,7 @@ void testSingleYaml() { * */ @Test - void cache(CapturedOutput output) { + void cache() { Secret red = new SecretBuilder().withNewMetadata() .withName("red") @@ -382,18 +382,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, + ReadType.BATCH); Fabric8ContextToSourceData redData = new NamedSecretContextToSourceDataProvider().get(); SourceData redSourceData = redData.apply(redContext); Assertions.assertThat(redSourceData.sourceName()).isEqualTo("secret.red.default"); Assertions.assertThat(redSourceData.sourceData().size()).isEqualTo(1); Assertions.assertThat(redSourceData.sourceData().get("some.color")).isEqualTo("red"); - Assertions.assertThat(output.getAll()).contains("Loaded all secrets in namespace '" + NAMESPACE + "'"); + + // delete the configmap, if caching is not present, the test would fail + mockClient.secrets().inNamespace(NAMESPACE).withName(green.getMetadata().getName()).delete(); 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, + ReadType.BATCH); Fabric8ContextToSourceData greenData = new NamedSecretContextToSourceDataProvider().get(); SourceData greenSourceData = greenData.apply(greenContext); @@ -401,14 +405,6 @@ void cache(CapturedOutput output) { Assertions.assertThat(greenSourceData.sourceData().size()).isEqualTo(1); Assertions.assertThat(greenSourceData.sourceData().get("some.taste")).isEqualTo("mango"); - // meaning there is a single entry with such a log statement - String[] out = output.getAll().split("Loaded all secrets in namespace"); - Assertions.assertThat(out.length).isEqualTo(2); - - // meaning that the second read was done from the cache - out = output.getAll().split("Loaded \\(from cache\\) all secrets in namespace"); - Assertions.assertThat(out.length).isEqualTo(2); - } } diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/NamedSecretContextToSourceDataProviderSingleBatchReadTests.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/NamedSecretContextToSourceDataProviderSingleBatchReadTests.java new file mode 100644 index 0000000000..d899b93254 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/NamedSecretContextToSourceDataProviderSingleBatchReadTests.java @@ -0,0 +1,417 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.config; + +import java.util.Base64; +import java.util.Collections; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.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.ReadType; +import org.springframework.cloud.kubernetes.commons.config.SourceData; +import org.springframework.mock.env.MockEnvironment; + +/** + * @author wind57 + */ +@EnableKubernetesMockClient(crud = true, https = false) +class NamedSecretContextToSourceDataProviderSingleBatchReadTests { + + 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(); + Fabric8SourcesBatchRead.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(), ReadType.SINGLE); + + Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("color", "really-red")); + + } + + /** + * we have three secret deployed. one of them has a name that matches (red), the other + * two have different names, thus no match. + */ + @Test + void twoSecretMatchAgainstLabels() { + + Secret red = new SecretBuilder().withNewMetadata() + .withName("red") + .endMetadata() + .addToData("color", Base64.getEncoder().encodeToString("really-red".getBytes())) + .build(); + + Secret blue = new SecretBuilder().withNewMetadata() + .withName("blue") + .endMetadata() + .addToData("color", Base64.getEncoder().encodeToString("really-blue".getBytes())) + .build(); + + Secret yellow = new SecretBuilder().withNewMetadata() + .withName("yellow") + .endMetadata() + .addToData("color", Base64.getEncoder().encodeToString("really-yellow".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(red).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(blue).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(yellow).create(); + + NormalizedSource normalizedSource = new NamedSecretNormalizedSource("red", NAMESPACE, true, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), ReadType.SINGLE); + + Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.default"); + Assertions.assertThat(sourceData.sourceData()).hasSize(1); + Assertions.assertThat(sourceData.sourceData().get("color")).isEqualTo("really-red"); + + } + + /** + * one secret deployed (pink), does not match our query (blue). + */ + @Test + void testSecretNoMatch() { + + Secret pink = new SecretBuilder().withNewMetadata() + .withName("pink") + .endMetadata() + .addToData("color", Base64.getEncoder().encodeToString("pink".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(pink).create(); + + NormalizedSource normalizedSource = new NamedSecretNormalizedSource("blue", NAMESPACE, true, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), ReadType.SINGLE); + + Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.blue.default"); + Assertions.assertThat(sourceData.sourceData()).isEmpty(); + } + + /** + * NamedSecretContextToSourceDataProvider gets as input a Fabric8ConfigContext. This + * context has a namespace as well as a NormalizedSource, that has a namespace too. It + * is easy to get confused in code on which namespace to use. This test makes sure + * that we use the proper one. + */ + @Test + void namespaceMatch() { + + Secret secret = new SecretBuilder().withNewMetadata() + .withName("red") + .endMetadata() + .addToData("color", Base64.getEncoder().encodeToString("really-red".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(secret).create(); + + // different namespace + NormalizedSource normalizedSource = new NamedSecretNormalizedSource("red", NAMESPACE + "nope", true, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), ReadType.SINGLE); + + Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("color", "really-red")); + } + + /** + * we have two secrets deployed. one matches the query name. the other matches the + * active profile + name, thus is taken also. + */ + @Test + void matchIncludeSingleProfile() { + + Secret red = new SecretBuilder().withNewMetadata() + .withName("red") + .endMetadata() + .addToData("color", Base64.getEncoder().encodeToString("really-red".getBytes())) + .build(); + + Secret redWithProfile = new SecretBuilder().withNewMetadata() + .withName("red-with-profile") + .endMetadata() + .addToData("taste", Base64.getEncoder().encodeToString("mango".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(red).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(redWithProfile).create(); + + // add one more profile and specify that we want profile based config maps + MockEnvironment env = new MockEnvironment(); + env.setActiveProfiles("with-profile"); + NormalizedSource normalizedSource = new NamedSecretNormalizedSource("red", NAMESPACE, true, + ConfigUtils.Prefix.DEFAULT, true, true); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env, + ReadType.SINGLE); + + Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.red-with-profile.default.with-profile"); + Assertions.assertThat(sourceData.sourceData()).hasSize(2); + Assertions.assertThat(sourceData.sourceData().get("color")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData().get("taste")).isEqualTo("mango"); + + } + + /** + * we have two secrets deployed. one matches the query name. the other matches the + * active profile + name, thus is taken also. This takes into consideration the + * prefix, that we explicitly specify. Notice that prefix works for profile based + * secrets as well. + */ + @Test + void matchIncludeSingleProfileWithPrefix() { + + Secret red = new SecretBuilder().withNewMetadata() + .withName("red") + .endMetadata() + .addToData("color", Base64.getEncoder().encodeToString("really-red".getBytes())) + .build(); + + Secret redWithProfile = new SecretBuilder().withNewMetadata() + .withName("red-with-profile") + .endMetadata() + .addToData("taste", Base64.getEncoder().encodeToString("mango".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(red).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(redWithProfile).create(); + + // add one more profile and specify that we want profile based config maps + // also append prefix + MockEnvironment env = new MockEnvironment(); + env.setActiveProfiles("with-profile"); + + NormalizedSource normalizedSource = new NamedSecretNormalizedSource("red", NAMESPACE, true, PREFIX, true); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env, + ReadType.SINGLE); + + Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.red-with-profile.default"); + Assertions.assertThat(sourceData.sourceData()).hasSize(2); + Assertions.assertThat(sourceData.sourceData().get("some.color")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData().get("some.taste")).isEqualTo("mango"); + + } + + /** + * we have three secrets deployed. one matches the query name. the other two match the + * active profile + name, thus are taken also. This takes into consideration the + * prefix, that we explicitly specify. Notice that prefix works for profile based + * config maps as well. + */ + @Test + void matchIncludeTwoProfilesWithPrefix() { + + Secret red = new SecretBuilder().withNewMetadata() + .withName("red") + .endMetadata() + .addToData("color", Base64.getEncoder().encodeToString("really-red".getBytes())) + .build(); + + Secret redWithTaste = new SecretBuilder().withNewMetadata() + .withName("red-with-taste") + .endMetadata() + .addToData("taste", Base64.getEncoder().encodeToString("mango".getBytes())) + .build(); + + Secret redWithShape = new SecretBuilder().withNewMetadata() + .withName("red-with-shape") + .endMetadata() + .addToData("shape", Base64.getEncoder().encodeToString("round".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(red).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(redWithTaste).create(); + mockClient.secrets().inNamespace(NAMESPACE).resource(redWithShape).create(); + + // add one more profile and specify that we want profile based config maps + // also append prefix + MockEnvironment env = new MockEnvironment(); + env.setActiveProfiles("with-taste", "with-shape"); + NormalizedSource normalizedSource = new NamedSecretNormalizedSource("red", NAMESPACE, true, PREFIX, true); + + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, env, + ReadType.SINGLE); + + Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.red.red-with-shape.red-with-taste.default"); + + Assertions.assertThat(sourceData.sourceData()).hasSize(3); + Assertions.assertThat(sourceData.sourceData().get("some.color")).isEqualTo("really-red"); + Assertions.assertThat(sourceData.sourceData().get("some.taste")).isEqualTo("mango"); + Assertions.assertThat(sourceData.sourceData().get("some.shape")).isEqualTo("round"); + + } + + /** + *
+	 *     - proves that single yaml file gets special treatment
+	 * 
+ */ + @Test + void testSingleYaml() { + Secret secret = new SecretBuilder().withNewMetadata() + .withName("single-yaml") + .endMetadata() + .addToData("single.yaml", Base64.getEncoder().encodeToString("key: value".getBytes())) + .build(); + + mockClient.secrets().inNamespace(NAMESPACE).resource(secret).create(); + + // different namespace + NormalizedSource normalizedSource = new NamedSecretNormalizedSource("single-yaml", NAMESPACE, true, false); + Fabric8ConfigContext context = new Fabric8ConfigContext(mockClient, normalizedSource, NAMESPACE, + new MockEnvironment(), ReadType.SINGLE); + + Fabric8ContextToSourceData data = new NamedSecretContextToSourceDataProvider().get(); + SourceData sourceData = data.apply(context); + + Assertions.assertThat(sourceData.sourceName()).isEqualTo("secret.single-yaml.default"); + Assertions.assertThat(sourceData.sourceData()) + .containsExactlyInAnyOrderEntriesOf(Collections.singletonMap("key", "value")); + } + + /** + *
+	 *     - two secrets are deployed : "red", "green", in the same namespace.
+	 *     - we first search for "red" and find it, and it is retrieved from the cluster via the client.
+	 *     - we then search for the "green" one, and it is not retrieved from the cache.
+	 * 
+ */ + @Test + void nonCache() { + + 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, + ReadType.SINGLE); + Fabric8ContextToSourceData redData = new NamedSecretContextToSourceDataProvider().get(); + SourceData redSourceData = redData.apply(redContext); + + Assertions.assertThat(redSourceData.sourceName()).isEqualTo("secret.red.default"); + Assertions.assertThat(redSourceData.sourceData()).hasSize(1); + Assertions.assertThat(redSourceData.sourceData().get("some.color")).isEqualTo("red"); + + NormalizedSource greenNormalizedSource = new NamedSecretNormalizedSource("green", NAMESPACE, true, PREFIX, + false); + Fabric8ConfigContext greenContext = new Fabric8ConfigContext(mockClient, greenNormalizedSource, NAMESPACE, env, + ReadType.SINGLE); + Fabric8ContextToSourceData greenData = new NamedSecretContextToSourceDataProvider().get(); + SourceData greenSourceData = greenData.apply(greenContext); + + Assertions.assertThat(greenSourceData.sourceName()).isEqualTo("secret.green.default"); + Assertions.assertThat(greenSourceData.sourceData()).hasSize(1); + Assertions.assertThat(greenSourceData.sourceData().get("some.taste")).isEqualTo("mango"); + + // since we do SINGLE reads, thus no caching is involved + // when we remove the configmap, nothing is found + mockClient.secrets().inNamespace(NAMESPACE).resource(green).delete(); + + greenData = new NamedSecretContextToSourceDataProvider().get(); + greenSourceData = greenData.apply(greenContext); + + Assertions.assertThat(greenSourceData.sourceData()).hasSize(0); + Assertions.assertThat(greenSourceData.sourceData().get("some.taste")).isNull(); + + } + +} 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 3f24aa9719..0e7996e368 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 @@ -26,6 +26,7 @@ import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.cloud.kubernetes.commons.config.RetryProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -73,7 +74,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, ReadType.BATCH); } } diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadConfigMapTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadConfigMapTest.java index efab354cec..c0fd79ec7d 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadConfigMapTest.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadConfigMapTest.java @@ -42,6 +42,7 @@ import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.cloud.kubernetes.commons.config.RetryProperties; import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy; @@ -63,7 +64,7 @@ classes = { EventReloadConfigMapTest.TestConfig.class }) @EnableKubernetesMockClient(crud = true) @ExtendWith(OutputCaptureExtension.class) -public class EventReloadConfigMapTest { +class EventReloadConfigMapTest { private static final boolean FAIL_FAST = false; @@ -162,7 +163,8 @@ AbstractEnvironment environment() { // simulate that environment already has a Fabric8ConfigMapPropertySource, // otherwise we can't properly test reload functionality ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), - List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT); + List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT, + ReadType.SINGLE); KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); PropertySource propertySource = new VisibleFabric8ConfigMapPropertySourceLocator(kubernetesClient, @@ -185,7 +187,7 @@ ConfigReloadProperties configReloadProperties() { @Primary ConfigMapConfigProperties configMapConfigProperties() { return new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, - false, true, FAIL_FAST, RetryProperties.DEFAULT); + false, true, FAIL_FAST, RetryProperties.DEFAULT, ReadType.SINGLE); } @Bean diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadSecretTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadSecretTest.java index 2ec89f62c9..c65f782af5 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadSecretTest.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/EventReloadSecretTest.java @@ -43,6 +43,7 @@ import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.cloud.kubernetes.commons.config.RetryProperties; import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties; import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; @@ -169,7 +170,8 @@ AbstractEnvironment environment() { // Fabric8SecretsPropertySourceLocator, // otherwise we can't properly test reload functionality SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), - List.of(), true, SECRET_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT); + List.of(), true, SECRET_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT, + ReadType.SINGLE); KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); PropertySource propertySource = new VisibleFabric8SecretsPropertySourceLocator(kubernetesClient, @@ -192,7 +194,7 @@ ConfigReloadProperties configReloadProperties() { @Primary SecretsConfigProperties secretsConfigProperties() { return new SecretsConfigProperties(true, Map.of(), List.of(), List.of(), true, SECRET_NAME, NAMESPACE, - false, true, FAIL_FAST, RetryProperties.DEFAULT); + false, true, FAIL_FAST, RetryProperties.DEFAULT, ReadType.SINGLE); } @Bean diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadConfigMapTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadConfigMapTest.java index f93a452d98..93b1e3bbe6 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadConfigMapTest.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadConfigMapTest.java @@ -39,6 +39,7 @@ import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.cloud.kubernetes.commons.config.RetryProperties; import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy; @@ -152,7 +153,8 @@ AbstractEnvironment environment() { // simulate that environment already has a Fabric8ConfigMapPropertySource, // otherwise we can't properly test reload functionality ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), - List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT); + List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT, + ReadType.BATCH); KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); PropertySource propertySource = new VisibleFabric8ConfigMapPropertySourceLocator(kubernetesClient, @@ -175,7 +177,7 @@ ConfigReloadProperties configReloadProperties() { @Primary ConfigMapConfigProperties configMapConfigProperties() { return new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, - false, true, FAIL_FAST, RetryProperties.DEFAULT); + false, true, FAIL_FAST, RetryProperties.DEFAULT, ReadType.BATCH); } @Bean diff --git a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadSecretTest.java b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadSecretTest.java index e2f8ecdc67..2819b1e502 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadSecretTest.java +++ b/spring-cloud-kubernetes-fabric8-config/src/test/java/org/springframework/cloud/kubernetes/fabric8/config/reload_it/PollingReloadSecretTest.java @@ -40,6 +40,7 @@ import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.config.ReadType; import org.springframework.cloud.kubernetes.commons.config.RetryProperties; import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties; import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties; @@ -65,7 +66,7 @@ @EnableKubernetesMockClient @ExtendWith(OutputCaptureExtension.class) -public class PollingReloadSecretTest { +class PollingReloadSecretTest { private static final boolean FAIL_FAST = false; @@ -159,7 +160,8 @@ AbstractEnvironment environment() { // simulate that environment already has a Fabric8SecretsPropertySource, // otherwise we can't properly test reload functionality SecretsConfigProperties secretsConfigProperties = new SecretsConfigProperties(true, Map.of(), List.of(), - List.of(), true, SECRET_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT); + List.of(), true, SECRET_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT, + ReadType.BATCH); KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment); PropertySource propertySource = new VisibleFabric8SecretsPropertySourceLocator(kubernetesClient, @@ -182,7 +184,7 @@ ConfigReloadProperties configReloadProperties() { @Primary SecretsConfigProperties secretsConfigProperties() { return new SecretsConfigProperties(true, Map.of(), List.of(), List.of(), true, SECRET_NAME, NAMESPACE, - false, true, FAIL_FAST, RetryProperties.DEFAULT); + false, true, FAIL_FAST, RetryProperties.DEFAULT, ReadType.BATCH); } @Bean