Skip to content

Commit dca7b19

Browse files
committed
add fabric8 it tests
1 parent c681f0a commit dca7b19

File tree

8 files changed

+933
-96
lines changed

8 files changed

+933
-96
lines changed

spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/reload/ConfigReloadUtil.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,11 @@ public static boolean reload(PropertySourceLocator locator, ConfigurableEnvironm
7272

7373
boolean changed = changed(sourceFromK8s, existingSources);
7474
if (changed) {
75-
LOG.info("Detected change in config maps/secrets");
75+
LOG.info("Detected change in config maps/secrets, reload will ne triggered");
7676
return true;
7777
}
7878
else {
79-
LOG.debug("No change detected in config maps/secrets, reload will not happen");
79+
LOG.debug("reloadable condition was not satisfied, reload will not be triggered");
8080
}
8181

8282
return false;
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,36 @@
1+
/*
2+
* Copyright 2013-2024 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+
117
package org.springframework.cloud.kubernetes.fabric8.config;
218

319
import io.fabric8.kubernetes.client.KubernetesClient;
20+
421
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
522
import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties;
623

24+
/**
25+
* Only needed to make Fabric8ConfigMapPropertySourceLocator visible for testing purposes
26+
*
27+
* @author wind57
28+
*/
729
public class VisibleFabric8ConfigMapPropertySourceLocator extends Fabric8ConfigMapPropertySourceLocator {
830

9-
public VisibleFabric8ConfigMapPropertySourceLocator(KubernetesClient client, ConfigMapConfigProperties properties, KubernetesNamespaceProvider provider) {
31+
public VisibleFabric8ConfigMapPropertySourceLocator(KubernetesClient client, ConfigMapConfigProperties properties,
32+
KubernetesNamespaceProvider provider) {
1033
super(client, properties, provider);
1134
}
35+
1236
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2013-2024 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.fabric8.config;
18+
19+
import io.fabric8.kubernetes.client.KubernetesClient;
20+
21+
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
22+
import org.springframework.cloud.kubernetes.commons.config.SecretsConfigProperties;
23+
24+
/**
25+
* Only needed to make Fabric8SecretsPropertySourceLocator visible for testing purposes
26+
*
27+
* @author wind57
28+
*/
29+
public class VisibleFabric8SecretsPropertySourceLocator extends Fabric8SecretsPropertySourceLocator {
30+
31+
public VisibleFabric8SecretsPropertySourceLocator(KubernetesClient client, SecretsConfigProperties properties,
32+
KubernetesNamespaceProvider provider) {
33+
super(client, properties, provider);
34+
}
35+
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/*
2+
* Copyright 2012-2024 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.fabric8.config.reload_it;
18+
19+
import java.time.Duration;
20+
import java.util.List;
21+
import java.util.Map;
22+
import java.util.Set;
23+
24+
import io.fabric8.kubernetes.api.model.ConfigMap;
25+
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
26+
import io.fabric8.kubernetes.api.model.ConfigMapList;
27+
import io.fabric8.kubernetes.client.KubernetesClient;
28+
import io.fabric8.kubernetes.client.dsl.MixedOperation;
29+
import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation;
30+
import io.fabric8.kubernetes.client.dsl.Resource;
31+
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
32+
import org.awaitility.Awaitility;
33+
import org.junit.jupiter.api.BeforeAll;
34+
import org.junit.jupiter.api.Test;
35+
import org.junit.jupiter.api.extension.ExtendWith;
36+
import org.mockito.Mockito;
37+
38+
import org.springframework.boot.test.context.SpringBootTest;
39+
import org.springframework.boot.test.context.TestConfiguration;
40+
import org.springframework.boot.test.system.CapturedOutput;
41+
import org.springframework.boot.test.system.OutputCaptureExtension;
42+
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
43+
import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties;
44+
import org.springframework.cloud.kubernetes.commons.config.RetryProperties;
45+
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties;
46+
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationUpdateStrategy;
47+
import org.springframework.cloud.kubernetes.fabric8.config.Fabric8ConfigMapPropertySourceLocator;
48+
import org.springframework.cloud.kubernetes.fabric8.config.VisibleFabric8ConfigMapPropertySourceLocator;
49+
import org.springframework.cloud.kubernetes.fabric8.config.reload.Fabric8EventBasedConfigMapChangeDetector;
50+
import org.springframework.context.annotation.Bean;
51+
import org.springframework.context.annotation.Primary;
52+
import org.springframework.core.env.AbstractEnvironment;
53+
import org.springframework.core.env.PropertySource;
54+
import org.springframework.mock.env.MockEnvironment;
55+
56+
/**
57+
* set 'spring.cloud.kubernetes.reload.enabled=false' so that auto-configuration does not
58+
* kick in, as we create our own config for the test here.
59+
*
60+
* @author wind57
61+
*/
62+
@SpringBootTest(
63+
properties = { "spring.main.cloud-platform=kubernetes", "spring.main.allow-bean-definition-overriding=true",
64+
"spring.cloud.kubernetes.reload.enabled=false",
65+
"logging.level.org.springframework.cloud.kubernetes.commons.config=debug" },
66+
classes = { EventReloadConfigMapTest.TestConfig.class })
67+
@EnableKubernetesMockClient(crud = true)
68+
@ExtendWith(OutputCaptureExtension.class)
69+
public class EventReloadConfigMapTest {
70+
71+
private static final boolean FAIL_FAST = false;
72+
73+
private static final String CONFIG_MAP_NAME = "mine";
74+
75+
private static final String NAMESPACE = "spring-k8s";
76+
77+
private static KubernetesClient kubernetesClient;
78+
79+
private static final boolean[] strategyCalled = new boolean[] { false };
80+
81+
@BeforeAll
82+
static void beforeAll() {
83+
84+
kubernetesClient = Mockito.spy(kubernetesClient);
85+
kubernetesClient.getConfiguration().setRequestRetryBackoffLimit(0);
86+
87+
ConfigMap configMapOne = configMap(CONFIG_MAP_NAME, Map.of());
88+
89+
// for the informer, when it starts
90+
kubernetesClient.configMaps().inNamespace(NAMESPACE).resource(configMapOne).create();
91+
}
92+
93+
@Test
94+
@SuppressWarnings({ "unchecked" })
95+
void test(CapturedOutput output) {
96+
97+
// we need to create this one before mocking calls
98+
NonNamespaceOperation<ConfigMap, ConfigMapList, Resource<ConfigMap>> operation = kubernetesClient.configMaps()
99+
.inNamespace(NAMESPACE);
100+
101+
// makes sure that when 'onEvent' is triggered (because we added a config map)
102+
// the call to /api/v1/namespaces/spring-k8s/configmaps will fail with an
103+
// Exception
104+
MixedOperation<ConfigMap, ConfigMapList, Resource<ConfigMap>> mixedOperation = Mockito
105+
.mock(MixedOperation.class);
106+
NonNamespaceOperation<ConfigMap, ConfigMapList, Resource<ConfigMap>> mockedOperation = Mockito
107+
.mock(NonNamespaceOperation.class);
108+
Mockito.when(kubernetesClient.configMaps()).thenReturn(mixedOperation);
109+
Mockito.when(mixedOperation.inNamespace(NAMESPACE)).thenReturn(mockedOperation);
110+
Mockito.when(mockedOperation.list()).thenThrow(new RuntimeException("failed in reading configmap"));
111+
112+
// create a different configmap that triggers even based reloading.
113+
// the one we create, will trigger a call to
114+
// /api/v1/namespaces/spring-k8s/configmaps
115+
// that we mocked above to fail.
116+
ConfigMap configMapTwo = configMap("not" + CONFIG_MAP_NAME, Map.of("a", "b"));
117+
operation.resource(configMapTwo).create();
118+
119+
// we fail while reading 'configMapTwo'
120+
Awaitility.await().atMost(Duration.ofSeconds(10)).pollInterval(Duration.ofSeconds(1)).until(() -> {
121+
boolean one = output.getOut().contains("failure in reading named sources");
122+
boolean two = output.getOut()
123+
.contains("there was an error while reading config maps/secrets, no reload will happen");
124+
boolean three = output.getOut()
125+
.contains("reloadable condition was not satisfied, reload will not be triggered");
126+
boolean updateStrategyNotCalled = !strategyCalled[0];
127+
return one && two && three && updateStrategyNotCalled;
128+
});
129+
130+
// reset the mock and replace our configmap with some data, so that reload
131+
// is triggered
132+
Mockito.reset(kubernetesClient);
133+
ConfigMap configMapOne = configMap(CONFIG_MAP_NAME, Map.of("a", "b"));
134+
operation.resource(configMapOne).replace();
135+
136+
// it passes while reading 'configMapThatWillPass'
137+
Awaitility.await()
138+
.atMost(Duration.ofSeconds(10))
139+
.pollInterval(Duration.ofSeconds(1))
140+
.until(() -> strategyCalled[0]);
141+
}
142+
143+
private static ConfigMap configMap(String name, Map<String, String> data) {
144+
return new ConfigMapBuilder().withNewMetadata().withName(name).endMetadata().withData(data).build();
145+
}
146+
147+
@TestConfiguration
148+
static class TestConfig {
149+
150+
@Bean
151+
@Primary
152+
Fabric8EventBasedConfigMapChangeDetector fabric8EventBasedSecretsChangeDetector(AbstractEnvironment environment,
153+
ConfigReloadProperties configReloadProperties, ConfigurationUpdateStrategy configurationUpdateStrategy,
154+
Fabric8ConfigMapPropertySourceLocator fabric8ConfigMapPropertySourceLocator,
155+
KubernetesNamespaceProvider namespaceProvider) {
156+
return new Fabric8EventBasedConfigMapChangeDetector(environment, configReloadProperties, kubernetesClient,
157+
configurationUpdateStrategy, fabric8ConfigMapPropertySourceLocator, namespaceProvider);
158+
}
159+
160+
@Bean
161+
@Primary
162+
AbstractEnvironment environment() {
163+
MockEnvironment mockEnvironment = new MockEnvironment();
164+
mockEnvironment.setProperty("spring.cloud.kubernetes.client.namespace", NAMESPACE);
165+
166+
// simulate that environment already has a Fabric8ConfigMapPropertySource,
167+
// otherwise we can't properly test reload functionality
168+
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(),
169+
List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE, false, true, true, RetryProperties.DEFAULT);
170+
KubernetesNamespaceProvider namespaceProvider = new KubernetesNamespaceProvider(mockEnvironment);
171+
172+
PropertySource<?> propertySource = new VisibleFabric8ConfigMapPropertySourceLocator(kubernetesClient,
173+
configMapConfigProperties, namespaceProvider)
174+
.locate(mockEnvironment);
175+
176+
mockEnvironment.getPropertySources().addFirst(propertySource);
177+
return mockEnvironment;
178+
}
179+
180+
@Bean
181+
@Primary
182+
ConfigReloadProperties configReloadProperties() {
183+
return new ConfigReloadProperties(true, true, false, ConfigReloadProperties.ReloadStrategy.REFRESH,
184+
ConfigReloadProperties.ReloadDetectionMode.EVENT, Duration.ofMillis(2000), Set.of(NAMESPACE), false,
185+
Duration.ofSeconds(2));
186+
}
187+
188+
@Bean
189+
@Primary
190+
ConfigMapConfigProperties configMapConfigProperties() {
191+
return new ConfigMapConfigProperties(true, List.of(), List.of(), Map.of(), true, CONFIG_MAP_NAME, NAMESPACE,
192+
false, true, FAIL_FAST, RetryProperties.DEFAULT);
193+
}
194+
195+
@Bean
196+
@Primary
197+
KubernetesNamespaceProvider namespaceProvider(AbstractEnvironment environment) {
198+
return new KubernetesNamespaceProvider(environment);
199+
}
200+
201+
@Bean
202+
@Primary
203+
ConfigurationUpdateStrategy configurationUpdateStrategy() {
204+
return new ConfigurationUpdateStrategy("to-console", () -> {
205+
strategyCalled[0] = true;
206+
});
207+
}
208+
209+
@Bean
210+
@Primary
211+
Fabric8ConfigMapPropertySourceLocator fabric8ConfigMapPropertySourceLocator(
212+
ConfigMapConfigProperties configMapConfigProperties, KubernetesNamespaceProvider namespaceProvider) {
213+
return new VisibleFabric8ConfigMapPropertySourceLocator(kubernetesClient, configMapConfigProperties,
214+
namespaceProvider);
215+
}
216+
217+
}
218+
219+
}

0 commit comments

Comments
 (0)