Skip to content

Commit 9b25290

Browse files
authored
Merge pull request #1594 from yue9944882/feat/spring-cfgmap-mapper
Feat(spring): Adding FromConfigMap annotation that dynamically maps data config-map to map
2 parents bef3b24 + f153a24 commit 9b25290

File tree

9 files changed

+514
-4
lines changed

9 files changed

+514
-4
lines changed

spring/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@
6464
<artifactId>wiremock</artifactId>
6565
<scope>test</scope>
6666
</dependency>
67+
<dependency>
68+
<groupId>org.awaitility</groupId>
69+
<artifactId>awaitility</artifactId>
70+
<scope>test</scope>
71+
</dependency>
6772

6873
</dependencies>
6974

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
Copyright 2021 The Kubernetes Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
package io.kubernetes.client.spring.extended.manifests;
14+
15+
import com.github.benmanes.caffeine.cache.CacheLoader;
16+
import com.github.benmanes.caffeine.cache.Caffeine;
17+
import com.github.benmanes.caffeine.cache.LoadingCache;
18+
import io.kubernetes.client.openapi.models.V1ConfigMap;
19+
import io.kubernetes.client.spring.extended.manifests.annotation.FromConfigMap;
20+
import io.kubernetes.client.spring.extended.manifests.config.KubernetesManifestsProperties;
21+
import io.kubernetes.client.spring.extended.manifests.configmaps.ConfigMapGetter;
22+
import java.lang.reflect.Field;
23+
import java.util.Map;
24+
import java.util.concurrent.Executors;
25+
import java.util.concurrent.ScheduledExecutorService;
26+
import java.util.concurrent.TimeUnit;
27+
import java.util.function.Supplier;
28+
import org.checkerframework.checker.nullness.qual.NonNull;
29+
import org.checkerframework.checker.nullness.qual.Nullable;
30+
import org.slf4j.Logger;
31+
import org.slf4j.LoggerFactory;
32+
import org.springframework.beans.BeansException;
33+
import org.springframework.beans.factory.BeanCreationException;
34+
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
35+
import org.springframework.beans.factory.annotation.Autowired;
36+
import org.springframework.beans.factory.config.BeanPostProcessor;
37+
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor;
38+
import org.springframework.context.ApplicationContext;
39+
import org.springframework.context.ApplicationContextAware;
40+
import org.springframework.util.ReflectionUtils;
41+
42+
public class KubernetesFromConfigMapProcessor
43+
implements InstantiationAwareBeanPostProcessor, BeanPostProcessor, ApplicationContextAware {
44+
45+
private static final Logger log = LoggerFactory.getLogger(KubernetesFromConfigMapProcessor.class);
46+
47+
private ApplicationContext applicationContext;
48+
49+
private final ScheduledExecutorService configMapKeyRefresher =
50+
Executors.newSingleThreadScheduledExecutor();
51+
52+
@Autowired private KubernetesManifestsProperties manifestsProperties;
53+
54+
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
55+
56+
for (Field field : bean.getClass().getDeclaredFields()) {
57+
ReflectionUtils.makeAccessible(field);
58+
try {
59+
if (field.get(bean) != null) {
60+
continue; // field already set, skip processing
61+
}
62+
} catch (IllegalAccessException e) {
63+
log.warn("Failed inject resource for @FromConfigMap annotated field {}", field, e);
64+
continue;
65+
}
66+
67+
FromConfigMap fromConfigMapAnnotation = field.getAnnotation(FromConfigMap.class);
68+
if (fromConfigMapAnnotation == null) {
69+
continue; // skip if the field doesn't have the annotation
70+
}
71+
72+
if (!Map.class.isAssignableFrom(field.getType())) {
73+
log.warn(
74+
"Failed inject resource for @FromConfigMap annotated field {}, the declaring type should be Map<String, String>",
75+
field);
76+
continue;
77+
}
78+
79+
ConfigMapGetter configMapGetter =
80+
getOrCreateConfigMapGetter(fromConfigMapAnnotation, applicationContext);
81+
82+
LoadingCache<String, String> configMapDataCache =
83+
Caffeine.newBuilder()
84+
.expireAfterWrite(manifestsProperties.getRefreshInterval())
85+
.build(
86+
new ConfigMapGetterCacheLoader(
87+
() -> {
88+
return configMapGetter.get(
89+
fromConfigMapAnnotation.namespace(), fromConfigMapAnnotation.name());
90+
}));
91+
fullyRefreshCache(configMapGetter, fromConfigMapAnnotation, configMapDataCache);
92+
configMapKeyRefresher.scheduleAtFixedRate(
93+
() -> {
94+
fullyRefreshCache(configMapGetter, fromConfigMapAnnotation, configMapDataCache);
95+
},
96+
manifestsProperties.getRefreshInterval().getSeconds(),
97+
manifestsProperties.getRefreshInterval().getSeconds(),
98+
TimeUnit.SECONDS);
99+
ReflectionUtils.setField(field, bean, configMapDataCache.asMap());
100+
}
101+
102+
return bean;
103+
}
104+
105+
private static void fullyRefreshCache(
106+
ConfigMapGetter configMapGetter,
107+
FromConfigMap fromConfigMapAnnotation,
108+
LoadingCache<String, String> configMapDataCache) {
109+
V1ConfigMap configMap =
110+
configMapGetter.get(fromConfigMapAnnotation.namespace(), fromConfigMapAnnotation.name());
111+
if (configMap == null || configMap.getData() == null) {
112+
return;
113+
}
114+
// TODO: make the cache data refreshment atomic
115+
configMap.getData().keySet().stream().forEach(key -> configMapDataCache.refresh(key));
116+
}
117+
118+
private ConfigMapGetter getOrCreateConfigMapGetter(
119+
FromConfigMap fromConfigMapAnnotation, ApplicationContext applicationContext) {
120+
ConfigMapGetter configMapGetter;
121+
try {
122+
configMapGetter =
123+
applicationContext
124+
.getAutowireCapableBeanFactory()
125+
.getBean(fromConfigMapAnnotation.configMapGetter());
126+
} catch (NoSuchBeanDefinitionException ne) {
127+
try {
128+
configMapGetter = fromConfigMapAnnotation.configMapGetter().newInstance();
129+
} catch (IllegalAccessException | InstantiationException e) {
130+
throw new BeanCreationException("failed creating configmap getter instance", e);
131+
}
132+
applicationContext.getAutowireCapableBeanFactory().autowireBean(configMapGetter);
133+
applicationContext
134+
.getAutowireCapableBeanFactory()
135+
.initializeBean(
136+
configMapGetter,
137+
"configmap-getter-" + fromConfigMapAnnotation.configMapGetter().getSimpleName());
138+
}
139+
return configMapGetter;
140+
}
141+
142+
@Override
143+
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
144+
this.applicationContext = applicationContext;
145+
}
146+
147+
static class ConfigMapGetterCacheLoader implements CacheLoader<String, String> {
148+
149+
ConfigMapGetterCacheLoader(Supplier<V1ConfigMap> configMapSupplier) {
150+
this.configMapSupplier = configMapSupplier;
151+
}
152+
153+
private final Supplier<V1ConfigMap> configMapSupplier;
154+
155+
@Override
156+
public @Nullable String load(@NonNull String key) throws Exception {
157+
V1ConfigMap configMap = this.configMapSupplier.get();
158+
if (configMap == null || configMap.getData() == null) {
159+
return null;
160+
}
161+
return configMap.getData().get(key);
162+
}
163+
}
164+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
Copyright 2021 The Kubernetes Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
package io.kubernetes.client.spring.extended.manifests.annotation;
14+
15+
import io.kubernetes.client.spring.extended.manifests.configmaps.ConfigMapGetter;
16+
import io.kubernetes.client.spring.extended.manifests.configmaps.PollingConfigMapGetter;
17+
import java.lang.annotation.ElementType;
18+
import java.lang.annotation.Retention;
19+
import java.lang.annotation.RetentionPolicy;
20+
import java.lang.annotation.Target;
21+
22+
/**
23+
* Injecting resources by reading from ConfigMap.
24+
*
25+
* <p>The annotations has to be be applied to member field of type Map<String, String>.
26+
*
27+
* <p>The content of the map will be automatically updated at the interval specified by the property
28+
* "kubernetes.manifests.refreshInterval".
29+
*
30+
* <p>If the given configmap, is not present in the cluster, the content of the map will stay empty.
31+
*/
32+
@Target({ElementType.FIELD})
33+
@Retention(RetentionPolicy.RUNTIME)
34+
public @interface FromConfigMap {
35+
36+
/**
37+
* Namespace of the configmap.
38+
*
39+
* @return the string
40+
*/
41+
String namespace();
42+
43+
/**
44+
* Name of the configmap
45+
*
46+
* @return the string
47+
*/
48+
String name();
49+
50+
/**
51+
* Config map getter class.
52+
*
53+
* @return the class
54+
*/
55+
Class<? extends ConfigMapGetter> configMapGetter() default PollingConfigMapGetter.class;
56+
}

spring/src/main/java/io/kubernetes/client/spring/extended/manifests/config/KubernetesManifestsAutoConfiguration.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,38 @@
1212
*/
1313
package io.kubernetes.client.spring.extended.manifests.config;
1414

15+
import io.kubernetes.client.spring.extended.manifests.KubernetesFromConfigMapProcessor;
1516
import io.kubernetes.client.spring.extended.manifests.KubernetesFromYamlProcessor;
1617
import io.kubernetes.client.spring.extended.manifests.KubernetesKubectlApplyProcessor;
1718
import io.kubernetes.client.spring.extended.manifests.KubernetesKubectlCreateProcessor;
1819
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
20+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
1921
import org.springframework.context.annotation.Bean;
2022
import org.springframework.context.annotation.Configuration;
2123

2224
@Configuration(proxyBeanMethods = false)
2325
@ConditionalOnKubernetesManifestsEnabled
26+
@EnableConfigurationProperties({
27+
KubernetesManifestsProperties.class,
28+
})
2429
public class KubernetesManifestsAutoConfiguration {
2530

2631
@Bean
2732
@ConditionalOnMissingBean
28-
public KubernetesKubectlCreateProcessor kubernetesKubectlCreateProcessor() {
29-
return new KubernetesKubectlCreateProcessor();
33+
public KubernetesFromYamlProcessor kubernetesFromYamlProcessor() {
34+
return new KubernetesFromYamlProcessor();
3035
}
3136

3237
@Bean
3338
@ConditionalOnMissingBean
34-
public KubernetesFromYamlProcessor kubernetesFromYamlProcessor() {
35-
return new KubernetesFromYamlProcessor();
39+
public KubernetesFromConfigMapProcessor kubernetesFromConfigMapProcessor() {
40+
return new KubernetesFromConfigMapProcessor();
41+
}
42+
43+
@Bean
44+
@ConditionalOnMissingBean
45+
public KubernetesKubectlCreateProcessor kubernetesKubectlCreateProcessor() {
46+
return new KubernetesKubectlCreateProcessor();
3647
}
3748

3849
@Bean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
Copyright 2021 The Kubernetes Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
package io.kubernetes.client.spring.extended.manifests.config;
14+
15+
import java.time.Duration;
16+
import org.springframework.boot.context.properties.ConfigurationProperties;
17+
18+
@ConfigurationProperties("kubernetes.manifests")
19+
public class KubernetesManifestsProperties {
20+
private Duration refreshInterval = Duration.ofSeconds(5);
21+
22+
public Duration getRefreshInterval() {
23+
return refreshInterval;
24+
}
25+
26+
public KubernetesManifestsProperties setRefreshInterval(Duration refreshInterval) {
27+
this.refreshInterval = refreshInterval;
28+
return this;
29+
}
30+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
Copyright 2021 The Kubernetes Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
package io.kubernetes.client.spring.extended.manifests.configmaps;
14+
15+
import io.kubernetes.client.openapi.models.V1ConfigMap;
16+
17+
public interface ConfigMapGetter {
18+
V1ConfigMap get(String namespace, String name);
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
Copyright 2021 The Kubernetes Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
package io.kubernetes.client.spring.extended.manifests.configmaps;
14+
15+
import io.kubernetes.client.informer.cache.Lister;
16+
import io.kubernetes.client.openapi.models.V1ConfigMap;
17+
import org.springframework.beans.factory.annotation.Autowired;
18+
19+
public class InformerConfigMapGetter implements ConfigMapGetter {
20+
21+
@Autowired private Lister<V1ConfigMap> configMapLister;
22+
23+
@Override
24+
public V1ConfigMap get(String namespace, String name) {
25+
return this.configMapLister.namespace(namespace).get(name);
26+
}
27+
}

0 commit comments

Comments
 (0)