Skip to content

Commit 663cc26

Browse files
authored
Merge pull request #2230 from wind57/drop_non_public_deprecated_method
drop internal deprecated methods in configuration watcher
2 parents f55dc76 + e8b1814 commit 663cc26

7 files changed

Lines changed: 469 additions & 91 deletions

File tree

docs/modules/ROOT/pages/spring-cloud-kubernetes-configuration-watcher.adoc

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,32 +39,51 @@ env:
3939
This one lets watcher know where to search for secrets and configmaps. You have two options here: selective namespaces (the setting above) and a namespace chosen by xref:property-source-config.adoc#namespace-resolution[Namespace Resolution] (this is the default option).
4040
Keep in mind that all these options require proper RBAC rules.
4141

42-
Changes from configmaps/secrets will only trigger an event being fired from configuration watcher if that particular change came from a source that has a label: `spring.cloud.kubernetes.config=true` or `spring.cloud.kubernetes.secret=true`.
42+
By default, Configuration Watcher monitors only ConfigMaps labeled:
4343

44-
To put it simpler, if you change a configmap (or secret), that does _not_ have the label above, configuration watcher will skip firing an event for it (if you enabled debug logging, this will be visible in logs).
44+
[source]
45+
----
46+
spring.cloud.kubernetes.config: "true"
47+
----
4548

46-
By default, configuration watcher will monitor all configmaps/secrets in the configured namespace(s). If you want to filter to watch only particular sources, you can do that by setting:
49+
Secrets are not monitored by default. To enable Secret monitoring, set:
4750

48-
[source]
51+
[source,yaml]
4952
----
50-
SPRING_CLOUD_KUBERNETES_CONFIG_INFORMER_ENABLED=TRUE
53+
env:
54+
- name: SPRING_CLOUD_KUBERNETES_SECRETS_ENABLED
55+
value: "TRUE"
5156
----
5257

53-
This will tell watcher to only monitor sources that have a label: `spring.cloud.kubernetes.config.informer.enabled=true`.
54-
This support will be dropped in the next major release and will be replaced with:
58+
Once enabled, only Secrets labeled: `spring.cloud.kubernetes.secret: "true"` are monitored.
5559

56-
[source]
60+
You can customize these label selectors in the Configuration Watcher application itself:
61+
62+
[source,yaml]
5763
----
58-
spring.cloud.kubernetes.reload.config-maps-labels
64+
spring:
65+
cloud:
66+
kubernetes:
67+
reload:
68+
config-maps-labels:
69+
my-configmap-label: "my-configmap-value"
70+
secrets-labels:
71+
my-secret-label: "my-secret-value"
5972
----
6073

61-
and
6274

63-
[source]
75+
or via environment variables in the watcher Deployment:
76+
77+
[source,yaml]
6478
----
65-
spring.cloud.kubernetes.reload.secrets-labels
79+
env:
80+
- name: SPRING_CLOUD_KUBERNETES_RELOAD_CONFIG_MAPS_LABELS_MY_CONFIGMAP_LABEL
81+
value: "my-configmap-value"
82+
- name: SPRING_CLOUD_KUBERNETES_RELOAD_SECRETS_LABELS_MY_SECRET_LABEL
83+
value: "my-secret-value"
6684
----
6785

86+
6887
One more important configuration, especially for configmaps and secrets that are mounted as volumes (via `spring.config.import`) is:
6988

7089
[source]

spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/KubernetesSource.java

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,6 @@ String description() {
3131
return "configmap";
3232
}
3333

34-
@Override
35-
String label() {
36-
return "spring.cloud.kubernetes.config";
37-
}
38-
3934
@Override
4035
String annotation() {
4136
return "spring.cloud.kubernetes.configmap.apps";
@@ -47,11 +42,6 @@ String description() {
4742
return "secret";
4843
}
4944

50-
@Override
51-
String label() {
52-
return "spring.cloud.kubernetes.secret";
53-
}
54-
5545
@Override
5646
String annotation() {
5747
return "spring.cloud.kubernetes.secret.apps";
@@ -61,8 +51,6 @@ String annotation() {
6151

6252
abstract String description();
6353

64-
abstract String label();
65-
6654
abstract String annotation();
6755

6856
static KubernetesSource fromK8sType(KubernetesObject kubernetesObject) {

spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/WatcherUtil.java

Lines changed: 7 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -53,43 +53,17 @@ static void onEvent(KubernetesObject kubernetesObject, long refreshDelay, Schedu
5353
KubernetesSource source = fromK8sType(kubernetesObject);
5454

5555
String name = kubernetesObject.getMetadata().getName();
56-
boolean isSpringCloudKubernetes = isSpringCloudKubernetes(kubernetesObject, source.label());
5756

58-
if (isSpringCloudKubernetes) {
59-
60-
Set<String> apps = apps(kubernetesObject, source.annotation());
61-
62-
if (apps.isEmpty()) {
63-
apps.add(name);
64-
}
65-
66-
LOG.info(() -> "will schedule remote refresh based on apps : " + apps);
67-
apps.forEach(appName -> schedule(source.description(), appName, refreshDelay, executorService,
68-
triggerRefresh, kubernetesObject));
57+
Set<String> apps = apps(kubernetesObject, source.annotation());
6958

59+
if (apps.isEmpty()) {
60+
apps.add(name);
7061
}
71-
else {
72-
LOG.debug(() -> "Not publishing event : " + source.description() + ": " + name
73-
+ " does not contain the label " + source.label());
74-
}
75-
}
7662

77-
/**
78-
* @deprecated for removal in the next major release, in favor of informer-side
79-
* filtering via {@code spring.cloud.kubernetes.reload.config-maps-labels} and
80-
* {@code spring.cloud.kubernetes.reload.secrets-labels}.
81-
* <p>
82-
* Today the configuration watcher receives all ConfigMap/Secret events from the
83-
* informer and filters them locally by legacy labels. With informer label selectors
84-
* configured, only matching sources are delivered, so this extra local check is no
85-
* longer needed.
86-
*/
87-
@Deprecated(forRemoval = true)
88-
static boolean isSpringCloudKubernetes(KubernetesObject kubernetesObject, String label) {
89-
if (kubernetesObject.getMetadata() == null) {
90-
return false;
91-
}
92-
return Boolean.parseBoolean(labels(kubernetesObject).getOrDefault(label, "false"));
63+
LOG.info(() -> "will schedule remote refresh based on apps : " + apps);
64+
apps.forEach(appName -> schedule(source.description(), appName, refreshDelay, executorService, triggerRefresh,
65+
kubernetesObject));
66+
9367
}
9468

9569
static Set<String> apps(KubernetesObject kubernetesObject, String annotationName) {

spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/resources/application.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,19 @@ spring:
55
name: spring-cloud-kubernetes-configuration-watcher
66
cloud:
77
kubernetes:
8+
secrets:
9+
enabled: false
10+
config:
11+
enabled: true
812
reload:
913
enabled: true
1014
monitoring-secrets: true
15+
monitoring-config-maps: true
1116
strategy: shutdown
17+
config-maps-labels:
18+
spring.cloud.kubernetes.config: true
19+
secrets-labels:
20+
spring.cloud.kubernetes.secret: true
1221
bus:
1322
enabled: false
1423
management:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
/*
2+
* Copyright 2013-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.kubernetes.configuration.watcher;
18+
19+
import java.util.List;
20+
import java.util.Map;
21+
import java.util.concurrent.CopyOnWriteArrayList;
22+
23+
import com.github.tomakehurst.wiremock.WireMockServer;
24+
import com.github.tomakehurst.wiremock.client.WireMock;
25+
import io.kubernetes.client.openapi.ApiClient;
26+
import io.kubernetes.client.openapi.JSON;
27+
import io.kubernetes.client.openapi.apis.CoreV1Api;
28+
import io.kubernetes.client.openapi.models.V1ConfigMap;
29+
import io.kubernetes.client.openapi.models.V1ConfigMapList;
30+
import io.kubernetes.client.openapi.models.V1ListMeta;
31+
import io.kubernetes.client.openapi.models.V1ObjectMeta;
32+
import io.kubernetes.client.util.ClientBuilder;
33+
import io.kubernetes.client.util.Watch;
34+
import org.assertj.core.api.Assertions;
35+
import org.junit.jupiter.api.AfterAll;
36+
import org.junit.jupiter.api.AfterEach;
37+
import org.junit.jupiter.api.BeforeAll;
38+
import org.junit.jupiter.api.Test;
39+
import org.mockito.MockedStatic;
40+
import org.mockito.Mockito;
41+
import reactor.core.publisher.Mono;
42+
43+
import org.springframework.boot.test.context.SpringBootTest;
44+
import org.springframework.boot.test.context.TestConfiguration;
45+
import org.springframework.cloud.kubernetes.client.KubernetesClientUtils;
46+
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
47+
import org.springframework.cloud.kubernetes.integration.tests.commons.Awaitilities;
48+
import org.springframework.context.annotation.Bean;
49+
50+
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
51+
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
52+
import static com.github.tomakehurst.wiremock.client.WireMock.get;
53+
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
54+
import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
55+
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
56+
import static io.kubernetes.client.informer.EventType.MODIFIED;
57+
58+
/**
59+
* @author wind57
60+
*/
61+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
62+
classes = ConfigMapWatcherWithLabelsTest.TestConfig.class,
63+
properties = { "spring.main.cloud-platform=KUBERNETES", "spring.config.import=",
64+
"spring.cloud.kubernetes.reload.enabled=false", "spring.cloud.kubernetes.discovery.enabled=false",
65+
"spring.cloud.kubernetes.reload.mode=EVENT",
66+
"spring.cloud.kubernetes.reload.monitoring-config-maps=true",
67+
"spring.cloud.kubernetes.reload.monitoring-secrets=false",
68+
"spring.cloud.kubernetes.reload.config-maps-labels[spring.cloud.kubernetes.config]=true",
69+
"spring.cloud.kubernetes.configuration.watcher.refresh-delay=1ms" })
70+
class ConfigMapWatcherWithLabelsTest {
71+
72+
private static final String PATH = "^/api/v1/namespaces/default/configmaps.*";
73+
74+
private static final MockedStatic<KubernetesClientUtils> KUBERNETES_CLIENT_UTILS_MOCKED_STATIC = Mockito
75+
.mockStatic(KubernetesClientUtils.class);
76+
77+
private static WireMockServer wireMockServer;
78+
79+
private static final List<String> OBSERVED_COLORS = new CopyOnWriteArrayList<>();
80+
81+
@BeforeAll
82+
static void beforeAll() {
83+
wireMockServer = new WireMockServer(options().dynamicPort());
84+
wireMockServer.start();
85+
WireMock.configureFor("localhost", wireMockServer.port());
86+
87+
ApiClient apiClient = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build();
88+
89+
KUBERNETES_CLIENT_UTILS_MOCKED_STATIC.when(() -> KubernetesClientUtils.getApplicationNamespace(Mockito.eq(null),
90+
Mockito.anyString(), Mockito.any(KubernetesNamespaceProvider.class)))
91+
.thenReturn("default");
92+
93+
KUBERNETES_CLIENT_UTILS_MOCKED_STATIC.when(KubernetesClientUtils::kubernetesApiClient).thenReturn(apiClient);
94+
95+
KUBERNETES_CLIENT_UTILS_MOCKED_STATIC.when(KubernetesClientUtils::createApiClientForInformerClient)
96+
.thenReturn(apiClient);
97+
98+
stubWatcher();
99+
100+
}
101+
102+
@AfterAll
103+
static void afterAll() {
104+
wireMockServer.stop();
105+
KUBERNETES_CLIENT_UTILS_MOCKED_STATIC.close();
106+
}
107+
108+
@AfterEach
109+
void afterEach() {
110+
WireMock.reset();
111+
}
112+
113+
/**
114+
* <pre>
115+
* There are two shared informers that are created in the configuration watcher
116+
* - HttpBasedConfigMapWatchChangeDetector that has onEvent that delegates to
117+
* WatcherUtil::onEvent
118+
* - KubernetesClientEventBasedConfigMapChangeDetector
119+
*
120+
* The first one is needed to be able to restart apps via the actuator, for example.
121+
* The second one is needed to reload properties of the configuration watcher itself.
122+
* In this test, we only care about the HttpBasedConfigMapWatchChangeDetector, as such we will set:
123+
*
124+
* spring.cloud.kubernetes.reload.enabled=false
125+
*
126+
* We set-up the informer to catch two calls : one where the configmap has color=white ( the first one )
127+
* and then color=blue, in the watch modified event.
128+
* </pre>
129+
*/
130+
@Test
131+
void test() {
132+
Awaitilities.awaitUntil(10, 1000, () -> OBSERVED_COLORS.size() == 2);
133+
Assertions.assertThat(OBSERVED_COLORS).containsExactly("white", "blue");
134+
}
135+
136+
private static void stubWatcher() {
137+
// ------------------------------------------------------------------------------------------------------------
138+
// 0. initial request of the informer ( resourceVersion=0 )
139+
// initial color is white
140+
141+
V1ConfigMap myConfigMapInitial = new V1ConfigMap()
142+
.metadata(new V1ObjectMeta().namespace("default")
143+
.labels(Map.of("spring.cloud.kubernetes.config", "true"))
144+
.name("my-configmap"))
145+
.data(Map.of("color", "white"));
146+
V1ConfigMapList myConfigMapListInitial = new V1ConfigMapList().metadata(new V1ListMeta().resourceVersion("1"))
147+
.items(List.of(myConfigMapInitial));
148+
149+
stubFor(get(urlMatching(PATH)).withQueryParam("watch", equalTo("false"))
150+
.withQueryParam("resourceVersion", equalTo("0"))
151+
.withQueryParam("labelSelector", equalTo("spring.cloud.kubernetes.config=true"))
152+
.willReturn(aResponse().withStatus(200).withBody(JSON.serialize(myConfigMapListInitial))));
153+
154+
// ------------------------------------------------------------------------------------------------------------
155+
// 1. first watch response to request with resourceVersion=1
156+
// color changed to blue
157+
158+
V1ConfigMap myConfigMapChanged = new V1ConfigMap()
159+
.metadata(new V1ObjectMeta().namespace("default")
160+
.labels(Map.of("spring.cloud.kubernetes.config", "true"))
161+
.name("my-configmap")
162+
.resourceVersion("2"))
163+
.data(Map.of("color", "blue"));
164+
165+
Watch.Response<V1ConfigMap> watchResponseOne = new Watch.Response<>(MODIFIED.name(), myConfigMapChanged);
166+
167+
stubFor(get(urlMatching(PATH)).withQueryParam("watch", equalTo("true"))
168+
.withQueryParam("resourceVersion", equalTo("1"))
169+
.withQueryParam("labelSelector", equalTo("spring.cloud.kubernetes.config=true"))
170+
.willReturn(aResponse().withStatus(200).withBody(JSON.serialize(watchResponseOne))));
171+
172+
// ------------------------------------------------------------------------------------------------------------
173+
// 2. all future calls to informer ( any call with resourceVersion >= 2 )
174+
stubFor(get(urlMatching(PATH)).atPriority(10)
175+
.withQueryParam("watch", equalTo("true"))
176+
.withQueryParam("resourceVersion", WireMock.matching("[2-9][0-9]*"))
177+
.withQueryParam("labelSelector", equalTo("spring.cloud.kubernetes.config=true"))
178+
.willReturn(aResponse().withStatus(200).withBody("")));
179+
}
180+
181+
@TestConfiguration
182+
static class TestConfig {
183+
184+
@Bean
185+
CoreV1Api coreV1Api() {
186+
return new CoreV1Api(new ClientBuilder().setBasePath(wireMockServer.baseUrl()).build());
187+
}
188+
189+
@Bean
190+
KubernetesNamespaceProvider kubernetesNamespaceProvider() {
191+
KubernetesNamespaceProvider namespaceProvider = Mockito.mock(KubernetesNamespaceProvider.class);
192+
Mockito.when(namespaceProvider.getNamespace()).thenReturn("default");
193+
return namespaceProvider;
194+
}
195+
196+
@Bean
197+
HttpRefreshTrigger httpRefreshTrigger() {
198+
HttpRefreshTrigger refreshTrigger = Mockito.mock(HttpRefreshTrigger.class);
199+
Mockito.when(refreshTrigger.triggerRefresh(Mockito.any(), Mockito.anyString())).thenAnswer(invocation -> {
200+
V1ConfigMap configMap = invocation.getArgument(0);
201+
return Mono.fromRunnable(() -> OBSERVED_COLORS.add(configMap.getData().get("color")));
202+
});
203+
204+
return refreshTrigger;
205+
}
206+
207+
}
208+
209+
}

0 commit comments

Comments
 (0)