.
+ *
+ * The content of the map will be automatically updated at the interval specified by the property
+ * "kubernetes.manifests.refreshInterval".
+ *
+ *
If the given configmap, is not present in the cluster, the content of the map will stay empty.
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface FromConfigMap {
+
+ /**
+ * Namespace of the configmap.
+ *
+ * @return the string
+ */
+ String namespace();
+
+ /**
+ * Name of the configmap
+ *
+ * @return the string
+ */
+ String name();
+
+ /**
+ * Config map getter class.
+ *
+ * @return the class
+ */
+ Class extends ConfigMapGetter> configMapGetter() default PollingConfigMapGetter.class;
+}
diff --git a/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/config/KubernetesManifestsAutoConfiguration.java b/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/config/KubernetesManifestsAutoConfiguration.java
index 6db6496c32..011a782881 100644
--- a/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/config/KubernetesManifestsAutoConfiguration.java
+++ b/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/config/KubernetesManifestsAutoConfiguration.java
@@ -12,27 +12,38 @@
*/
package io.kubernetes.client.spring.extended.manifests.config;
+import io.kubernetes.client.spring.extended.manifests.KubernetesFromConfigMapProcessor;
import io.kubernetes.client.spring.extended.manifests.KubernetesFromYamlProcessor;
import io.kubernetes.client.spring.extended.manifests.KubernetesKubectlApplyProcessor;
import io.kubernetes.client.spring.extended.manifests.KubernetesKubectlCreateProcessor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
@ConditionalOnKubernetesManifestsEnabled
+@EnableConfigurationProperties({
+ KubernetesManifestsProperties.class,
+})
public class KubernetesManifestsAutoConfiguration {
@Bean
@ConditionalOnMissingBean
- public KubernetesKubectlCreateProcessor kubernetesKubectlCreateProcessor() {
- return new KubernetesKubectlCreateProcessor();
+ public KubernetesFromYamlProcessor kubernetesFromYamlProcessor() {
+ return new KubernetesFromYamlProcessor();
}
@Bean
@ConditionalOnMissingBean
- public KubernetesFromYamlProcessor kubernetesFromYamlProcessor() {
- return new KubernetesFromYamlProcessor();
+ public KubernetesFromConfigMapProcessor kubernetesFromConfigMapProcessor() {
+ return new KubernetesFromConfigMapProcessor();
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public KubernetesKubectlCreateProcessor kubernetesKubectlCreateProcessor() {
+ return new KubernetesKubectlCreateProcessor();
}
@Bean
diff --git a/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/config/KubernetesManifestsProperties.java b/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/config/KubernetesManifestsProperties.java
new file mode 100644
index 0000000000..ec5c8bd8b0
--- /dev/null
+++ b/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/config/KubernetesManifestsProperties.java
@@ -0,0 +1,30 @@
+/*
+Copyright 2021 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.config;
+
+import java.time.Duration;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties("kubernetes.manifests")
+public class KubernetesManifestsProperties {
+ private Duration refreshInterval = Duration.ofSeconds(5);
+
+ public Duration getRefreshInterval() {
+ return refreshInterval;
+ }
+
+ public KubernetesManifestsProperties setRefreshInterval(Duration refreshInterval) {
+ this.refreshInterval = refreshInterval;
+ return this;
+ }
+}
diff --git a/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/configmaps/ConfigMapGetter.java b/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/configmaps/ConfigMapGetter.java
new file mode 100644
index 0000000000..ce77616f3b
--- /dev/null
+++ b/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/configmaps/ConfigMapGetter.java
@@ -0,0 +1,19 @@
+/*
+Copyright 2021 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.configmaps;
+
+import io.kubernetes.client.openapi.models.V1ConfigMap;
+
+public interface ConfigMapGetter {
+ V1ConfigMap get(String namespace, String name);
+}
diff --git a/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/configmaps/InformerConfigMapGetter.java b/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/configmaps/InformerConfigMapGetter.java
new file mode 100644
index 0000000000..140422ce55
--- /dev/null
+++ b/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/configmaps/InformerConfigMapGetter.java
@@ -0,0 +1,27 @@
+/*
+Copyright 2021 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.configmaps;
+
+import io.kubernetes.client.informer.cache.Lister;
+import io.kubernetes.client.openapi.models.V1ConfigMap;
+import org.springframework.beans.factory.annotation.Autowired;
+
+public class InformerConfigMapGetter implements ConfigMapGetter {
+
+ @Autowired private Lister configMapLister;
+
+ @Override
+ public V1ConfigMap get(String namespace, String name) {
+ return this.configMapLister.namespace(namespace).get(name);
+ }
+}
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
new file mode 100644
index 0000000000..34d0ee9840
--- /dev/null
+++ b/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/configmaps/PollingConfigMapGetter.java
@@ -0,0 +1,45 @@
+/*
+Copyright 2020 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.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 {
+
+ private static final Cache lastObservedConfigMap =
+ Caffeine.newBuilder().expireAfterWrite(Duration.ofMinutes(5)).build();
+
+ @Autowired private ApiClient apiClient;
+
+ @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, null, null, null);
+ } catch (ApiException e) {
+ throw new IllegalStateException(e);
+ }
+ });
+ }
+}
diff --git a/spring/src/test/java/io/kubernetes/client/spring/extended/manifests/KubernetesFromConfigMapTest.java b/spring/src/test/java/io/kubernetes/client/spring/extended/manifests/KubernetesFromConfigMapTest.java
new file mode 100644
index 0000000000..ca83a33da7
--- /dev/null
+++ b/spring/src/test/java/io/kubernetes/client/spring/extended/manifests/KubernetesFromConfigMapTest.java
@@ -0,0 +1,153 @@
+/*
+Copyright 2021 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 junit.framework.Assert.assertNull;
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertNotNull;
+
+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 io.kubernetes.client.spring.extended.manifests.configmaps.ConfigMapGetter;
+import java.time.Duration;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+import org.awaitility.Awaitility;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.model.Statement;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(
+ classes = KubernetesFromConfigMapTest.App.class,
+ properties = {
+ "kubernetes.manifests.refreshInterval=1s",
+ })
+public class KubernetesFromConfigMapTest {
+
+ @Rule public ConfigMapResetter configMapResetter = new ConfigMapResetter();
+
+ @SpringBootConfiguration
+ @EnableAutoConfiguration
+ static class App {
+
+ @Bean
+ public KubernetesFromConfigMapProcessor kubernetesFromConfigMapProcessor() {
+ return new KubernetesFromConfigMapProcessor();
+ }
+
+ @Bean
+ public MockAtomicConfigMapGetter mockAtomicConfigMapGetter() {
+ MockAtomicConfigMapGetter atomicConfigMapGetter = new MockAtomicConfigMapGetter();
+ return atomicConfigMapGetter;
+ }
+
+ @Bean
+ public KubernetesFromConfigMapTest.MyBean myBean() {
+ return new KubernetesFromConfigMapTest.MyBean();
+ }
+ }
+
+ static class MyBean {
+ @FromConfigMap(namespace = "default", name = "foo", configMapGetter = MockConfigMapGetter.class)
+ private Map staticData;
+
+ @FromConfigMap(
+ namespace = "default",
+ name = "foo",
+ configMapGetter = MockAtomicConfigMapGetter.class)
+ private Map dynamicData;
+ }
+
+ @Autowired private KubernetesFromConfigMapTest.MyBean myBean;
+
+ @Autowired private MockAtomicConfigMapGetter mockAtomicConfigMapGetter;
+
+ @Autowired private KubernetesManifestsProperties manifestsProperties;
+
+ @Test
+ public void testReadOnce() {
+ assertNotNull(myBean.staticData);
+ assertEquals("bar", myBean.staticData.get("foo"));
+ }
+
+ @Test
+ public void testValueUpdate() throws InterruptedException {
+ assertEquals(Duration.ofSeconds(1), manifestsProperties.getRefreshInterval());
+ assertNotNull(myBean.dynamicData);
+ assertEquals("bar1", myBean.dynamicData.get("foo"));
+ mockAtomicConfigMapGetter.configMapAtomicReference.set(
+ new V1ConfigMap().putDataItem("foo", "bar2"));
+ Thread.sleep(manifestsProperties.getRefreshInterval().toMillis());
+ assertEquals("bar2", myBean.dynamicData.get("foo"));
+ }
+
+ @Test
+ public void testKeyUpdate() throws InterruptedException {
+ assertEquals(Duration.ofSeconds(1), manifestsProperties.getRefreshInterval());
+ assertNotNull(myBean.dynamicData);
+ assertEquals("bar1", myBean.dynamicData.get("foo"));
+ mockAtomicConfigMapGetter.configMapAtomicReference.set(
+ new V1ConfigMap().putDataItem("foo1", "bar"));
+ Thread.sleep(manifestsProperties.getRefreshInterval().toMillis());
+ assertNull(myBean.dynamicData.get("foo")); // old key should be removed
+ assertEquals("bar", myBean.dynamicData.get("foo1")); // new key should be added
+ }
+
+ private void reset() {
+ mockAtomicConfigMapGetter.configMapAtomicReference.set(
+ new V1ConfigMap().putDataItem("foo", "bar1"));
+ }
+
+ static class MockConfigMapGetter implements ConfigMapGetter {
+ @Override
+ public V1ConfigMap get(String namespace, String name) {
+ return new V1ConfigMap().putDataItem("foo", "bar");
+ }
+ }
+
+ static class MockAtomicConfigMapGetter implements ConfigMapGetter {
+
+ private final AtomicReference configMapAtomicReference = new AtomicReference<>();
+
+ @Override
+ public V1ConfigMap get(String namespace, String name) {
+ return configMapAtomicReference.get();
+ }
+ }
+
+ class ConfigMapResetter implements TestRule {
+ @Override
+ public Statement apply(Statement statement, Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ mockAtomicConfigMapGetter.configMapAtomicReference.set(
+ new V1ConfigMap().putDataItem("foo", "bar1"));
+ Awaitility.await().until(() -> "bar1".equals(myBean.dynamicData.get("foo")));
+ statement.evaluate();
+ }
+ };
+ }
+ }
+}