diff --git a/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/annotation/FromConfigMap.java b/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/annotation/FromConfigMap.java index 240bee9aa1..731bc9db52 100644 --- a/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/annotation/FromConfigMap.java +++ b/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/annotation/FromConfigMap.java @@ -25,7 +25,8 @@ *

The annotations has to be be applied to member field of type Map. * *

The content of the map will be automatically updated at the interval specified by the property - * "kubernetes.manifests.refreshInterval". + * "kubernetes.manifests.refreshInterval". The default polling getter reads the ConfigMap from the + * API server on every refresh. * *

If the given configmap, is not present in the cluster, the content of the map will stay empty. */ diff --git a/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/configmaps/PollingConfigMapGetter.java b/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/configmaps/PollingConfigMapGetter.java index d9249e29c2..a0db4eecda 100644 --- a/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/configmaps/PollingConfigMapGetter.java +++ b/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/configmaps/PollingConfigMapGetter.java @@ -12,14 +12,10 @@ */ package io.kubernetes.client.spring.extended.manifests.configmaps; -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; -import io.kubernetes.client.apimachinery.NamespaceName; import io.kubernetes.client.openapi.ApiClient; import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.apis.CoreV1Api; import io.kubernetes.client.openapi.models.V1ConfigMap; -import java.time.Duration; import org.springframework.beans.factory.annotation.Autowired; public class PollingConfigMapGetter implements ConfigMapGetter { @@ -32,14 +28,10 @@ public class PollingConfigMapGetter implements ConfigMapGetter { @Override public V1ConfigMap get(String namespace, String name) { CoreV1Api coreV1Api = new CoreV1Api(apiClient); - return lastObservedConfigMap.get( - new NamespaceName(namespace, name), - k -> { - try { - return coreV1Api.readNamespacedConfigMap(name, namespace).execute(); - } catch (ApiException e) { - throw new IllegalStateException(e); - } - }); + try { + return coreV1Api.readNamespacedConfigMap(name, namespace).execute(); + } catch (ApiException e) { + throw new IllegalStateException(e); + } } } diff --git a/spring/src/test/java/io/kubernetes/client/spring/extended/manifests/KubernetesFromConfigMapPollingTest.java b/spring/src/test/java/io/kubernetes/client/spring/extended/manifests/KubernetesFromConfigMapPollingTest.java new file mode 100644 index 0000000000..9828a79ce5 --- /dev/null +++ b/spring/src/test/java/io/kubernetes/client/spring/extended/manifests/KubernetesFromConfigMapPollingTest.java @@ -0,0 +1,98 @@ +/* +Copyright 2026 The Kubernetes 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 +http://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 io.kubernetes.client.spring.extended.manifests; + +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.urlPathEqualTo; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.models.V1ConfigMap; +import io.kubernetes.client.spring.extended.manifests.annotation.FromConfigMap; +import io.kubernetes.client.spring.extended.manifests.config.KubernetesManifestsProperties; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.test.util.ReflectionTestUtils; + +class KubernetesFromConfigMapPollingTest { + + private static final String CONFIG_MAP_PATH = "/api/v1/namespaces/default/configmaps/polling"; + + @RegisterExtension + static WireMockExtension apiServer = + WireMockExtension.newInstance() + .options(WireMockConfiguration.options().dynamicPort()) + .build(); + + @Test + void refreshesFromApiServerAtConfiguredInterval() { + ApiClient apiClient = new ApiClient().setBasePath(apiServer.baseUrl()); + KubernetesManifestsProperties properties = + new KubernetesManifestsProperties().setRefreshInterval(Duration.ofSeconds(1)); + KubernetesFromConfigMapProcessor processor = new KubernetesFromConfigMapProcessor(); + + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + context.registerBean(ApiClient.class, () -> apiClient); + context.registerBean(KubernetesManifestsProperties.class, () -> properties); + context.refresh(); + context.getAutowireCapableBeanFactory().autowireBean(processor); + processor.setApplicationContext(context); + + stubConfigMap( + apiClient, + new V1ConfigMap().putDataItem("foo", "original").putDataItem("removed", "value")); + MyBean bean = new MyBean(); + processor.postProcessAfterInitialization(bean, "myBean"); + assertThat(bean.data).containsEntry("foo", "original").containsKey("removed"); + apiServer.verify(getRequestedFor(urlPathEqualTo(CONFIG_MAP_PATH))); + + apiServer.resetRequests(); + stubConfigMap( + apiClient, new V1ConfigMap().putDataItem("foo", "updated").putDataItem("added", "value")); + await() + .atMost(properties.getRefreshInterval().multipliedBy(5)) + .untilAsserted( + () -> + assertThat(bean.data) + .containsExactlyInAnyOrderEntriesOf( + Map.of("foo", "updated", "added", "value"))); + apiServer.verify(getRequestedFor(urlPathEqualTo(CONFIG_MAP_PATH))); + } finally { + ((ScheduledExecutorService) ReflectionTestUtils.getField(processor, "configMapKeyRefresher")) + .shutdownNow(); + } + } + + private static void stubConfigMap(ApiClient apiClient, V1ConfigMap configMap) { + apiServer.stubFor( + get(urlPathEqualTo(CONFIG_MAP_PATH)) + .willReturn( + aResponse() + .withHeader("Content-Type", "application/json") + .withBody(apiClient.getJSON().serialize(configMap)))); + } + + static class MyBean { + @FromConfigMap(namespace = "default", name = "polling") + private Map data; + } +}