Skip to content

Commit 4c3df93

Browse files
committed
add k8s client tests
1 parent 804458f commit 4c3df93

File tree

1 file changed

+374
-0
lines changed

1 file changed

+374
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
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.client.config;
18+
19+
import java.util.List;
20+
import java.util.Map;
21+
22+
import com.github.tomakehurst.wiremock.WireMockServer;
23+
import com.github.tomakehurst.wiremock.client.WireMock;
24+
import io.kubernetes.client.openapi.ApiClient;
25+
import io.kubernetes.client.openapi.Configuration;
26+
import io.kubernetes.client.openapi.JSON;
27+
import io.kubernetes.client.openapi.apis.CoreV1Api;
28+
import io.kubernetes.client.openapi.models.V1ConfigMapBuilder;
29+
import io.kubernetes.client.openapi.models.V1ConfigMapList;
30+
import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder;
31+
import io.kubernetes.client.util.ClientBuilder;
32+
import org.junit.jupiter.api.AfterAll;
33+
import org.junit.jupiter.api.AfterEach;
34+
import org.junit.jupiter.api.BeforeAll;
35+
import org.junit.jupiter.api.Test;
36+
import org.junit.jupiter.api.extension.ExtendWith;
37+
38+
import org.springframework.boot.test.system.CapturedOutput;
39+
import org.springframework.boot.test.system.OutputCaptureExtension;
40+
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
41+
import org.springframework.cloud.kubernetes.commons.config.ConfigMapConfigProperties;
42+
import org.springframework.cloud.kubernetes.commons.config.RetryProperties;
43+
import org.springframework.cloud.kubernetes.commons.config.SourceData;
44+
import org.springframework.core.env.CompositePropertySource;
45+
import org.springframework.core.env.MapPropertySource;
46+
import org.springframework.core.env.PropertySource;
47+
import org.springframework.mock.env.MockEnvironment;
48+
49+
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
50+
import static com.github.tomakehurst.wiremock.client.WireMock.get;
51+
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
52+
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
53+
import static org.assertj.core.api.Assertions.assertThat;
54+
55+
/**
56+
* @author wind57
57+
*/
58+
@ExtendWith(OutputCaptureExtension.class)
59+
class KubernetesClientConfigMapErrorOnReadingSourceTests {
60+
61+
private static final V1ConfigMapList SINGLE_CONFIGMAP_LIST = new V1ConfigMapList()
62+
.addItemsItem(new V1ConfigMapBuilder()
63+
.withMetadata(
64+
new V1ObjectMetaBuilder().withName("two").withNamespace("default").withResourceVersion("1").build())
65+
.build());
66+
67+
private static final V1ConfigMapList DOUBLE_CONFIGMAP_LIST = new V1ConfigMapList()
68+
.addItemsItem(new V1ConfigMapBuilder()
69+
.withMetadata(
70+
new V1ObjectMetaBuilder().withName("one").withNamespace("default").withResourceVersion("1").build())
71+
.build())
72+
.addItemsItem(new V1ConfigMapBuilder()
73+
.withMetadata(
74+
new V1ObjectMetaBuilder().withName("two").withNamespace("default").withResourceVersion("1").build())
75+
.build());
76+
77+
private static WireMockServer wireMockServer;
78+
79+
@BeforeAll
80+
public static void setup() {
81+
wireMockServer = new WireMockServer(options().dynamicPort());
82+
83+
wireMockServer.start();
84+
WireMock.configureFor("localhost", wireMockServer.port());
85+
86+
ApiClient client = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build();
87+
client.setDebugging(true);
88+
Configuration.setDefaultApiClient(client);
89+
}
90+
91+
@AfterAll
92+
public static void after() {
93+
wireMockServer.stop();
94+
}
95+
96+
@AfterEach
97+
public void afterEach() {
98+
WireMock.reset();
99+
}
100+
101+
/**
102+
* <pre>
103+
* we try to read all config maps in a namespace and fail,
104+
* thus generate a well defined name for the source.
105+
* </pre>
106+
*/
107+
@Test
108+
void namedSingleConfigMapFails() {
109+
String name = "my-config";
110+
String namespace = "spring-k8s";
111+
String path = "/api/v1/namespaces/" + namespace + "/configmaps";
112+
113+
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error")));
114+
115+
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(), List.of(),
116+
Map.of(), true, name, namespace, false, true, false, RetryProperties.DEFAULT);
117+
118+
CoreV1Api api = new CoreV1Api();
119+
KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api,
120+
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
121+
122+
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment());
123+
MapPropertySource mapPropertySource = (MapPropertySource) propertySource.getPropertySources()
124+
.stream()
125+
.findAny()
126+
.orElseThrow();
127+
128+
assertThat(mapPropertySource.getName()).isEqualTo(SourceData.EMPTY_SOURCE_NAME_ON_ERROR);
129+
130+
}
131+
132+
/**
133+
* <pre>
134+
* there are two sources and we try to read them.
135+
* one fails and one passes.
136+
* </pre>
137+
*/
138+
@Test
139+
void namedTwoConfigMapsOneFails(CapturedOutput output) {
140+
String configMapNameOne = "one";
141+
String configMapNameTwo = "two";
142+
String namespace = "default";
143+
String path = "/api/v1/namespaces/default/configmaps";
144+
145+
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))
146+
.inScenario("started")
147+
.willSetStateTo("go-to-next"));
148+
149+
stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(SINGLE_CONFIGMAP_LIST)))
150+
.inScenario("started")
151+
.whenScenarioStateIs("go-to-next")
152+
.willSetStateTo("done"));
153+
154+
ConfigMapConfigProperties.Source sourceOne = new ConfigMapConfigProperties.Source(configMapNameOne, namespace,
155+
Map.of(), null, null, null);
156+
ConfigMapConfigProperties.Source sourceTwo = new ConfigMapConfigProperties.Source(configMapNameTwo, namespace,
157+
Map.of(), null, null, null);
158+
159+
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(),
160+
List.of(sourceOne, sourceTwo), Map.of(), true, null, namespace, false, true, false,
161+
RetryProperties.DEFAULT);
162+
163+
CoreV1Api api = new CoreV1Api();
164+
KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api,
165+
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
166+
167+
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment());
168+
List<String> names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList();
169+
170+
// two sources are present, one being empty
171+
assertThat(names).containsExactly("configmap.two.default", SourceData.EMPTY_SOURCE_NAME_ON_ERROR);
172+
assertThat(output.getOut())
173+
.doesNotContain("sourceName : two was requested, but not found in namespace : default");
174+
175+
}
176+
177+
/**
178+
* <pre>
179+
* there are two sources and we try to read them.
180+
* both fail.
181+
* </pre>
182+
*/
183+
@Test
184+
void namedTwoConfigMapsBothFail(CapturedOutput output) {
185+
String configMapNameOne = "one";
186+
String configMapNameTwo = "two";
187+
String namespace = "default";
188+
String path = "/api/v1/namespaces/default/configmaps";
189+
190+
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))
191+
.inScenario("started")
192+
.willSetStateTo("go-to-next"));
193+
194+
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))
195+
.inScenario("started")
196+
.whenScenarioStateIs("go-to-next")
197+
.willSetStateTo("done"));
198+
199+
ConfigMapConfigProperties.Source sourceOne = new ConfigMapConfigProperties.Source(configMapNameOne, namespace,
200+
Map.of(), null, null, null);
201+
ConfigMapConfigProperties.Source sourceTwo = new ConfigMapConfigProperties.Source(configMapNameTwo, namespace,
202+
Map.of(), null, null, null);
203+
204+
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(),
205+
List.of(sourceOne, sourceTwo), Map.of(), true, null, namespace, false, true, false,
206+
RetryProperties.DEFAULT);
207+
208+
CoreV1Api api = new CoreV1Api();
209+
KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api,
210+
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
211+
212+
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment());
213+
List<String> names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList();
214+
215+
assertThat(names).containsExactly(SourceData.EMPTY_SOURCE_NAME_ON_ERROR);
216+
assertThat(output.getOut())
217+
.doesNotContain("sourceName : one was requested, but not found in namespace : default");
218+
assertThat(output.getOut())
219+
.doesNotContain("sourceName : two was requested, but not found in namespace : default");
220+
}
221+
222+
/**
223+
* <pre>
224+
* we try to read all config maps in a namespace and fail,
225+
* thus generate a well defined name for the source.
226+
* </pre>
227+
*/
228+
@Test
229+
void labeledSingleConfigMapFails(CapturedOutput output) {
230+
Map<String, String> labels = Map.of("a", "b");
231+
String namespace = "spring-k8s";
232+
String path = "/api/v1/namespaces/" + namespace + "/configmaps";
233+
234+
// one for the 'application' named configmap
235+
// the other for the labeled config map
236+
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))
237+
.inScenario("started")
238+
.willSetStateTo("go-to-next"));
239+
240+
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))
241+
.inScenario("started")
242+
.whenScenarioStateIs("go-to-next")
243+
.willSetStateTo("done"));
244+
245+
ConfigMapConfigProperties.Source configMapSource = new ConfigMapConfigProperties.Source(null, namespace, labels,
246+
null, null, null);
247+
248+
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(),
249+
List.of(configMapSource), labels, true, null, namespace, false, true, false, RetryProperties.DEFAULT);
250+
251+
CoreV1Api api = new CoreV1Api();
252+
KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api,
253+
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
254+
255+
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment());
256+
List<String> sourceNames = propertySource.getPropertySources().stream().map(PropertySource::getName).toList();
257+
258+
assertThat(sourceNames).containsExactly(SourceData.EMPTY_SOURCE_NAME_ON_ERROR);
259+
assertThat(output).contains("failure in reading labeled sources");
260+
assertThat(output).contains("failure in reading named sources");
261+
}
262+
263+
/**
264+
* <pre>
265+
* there are two sources and we try to read them.
266+
* one fails and one passes.
267+
* </pre>
268+
*/
269+
@Test
270+
void labeledTwoConfigMapsOneFails(CapturedOutput output) {
271+
String configMapNameOne = "one";
272+
String configMapNameTwo = "two";
273+
274+
Map<String, String> configMapOneLabels = Map.of("one", "1");
275+
Map<String, String> configMapTwoLabels = Map.of("two", "2");
276+
277+
String namespace = "default";
278+
String path = "/api/v1/namespaces/default/configmaps";
279+
280+
// one for 'application' named configmap and one for the first labeled configmap
281+
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))
282+
.inScenario("started")
283+
.willSetStateTo("first"));
284+
285+
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))
286+
.inScenario("started")
287+
.whenScenarioStateIs("first")
288+
.willSetStateTo("second"));
289+
290+
// one that passes
291+
stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(DOUBLE_CONFIGMAP_LIST)))
292+
.inScenario("started")
293+
.whenScenarioStateIs("second")
294+
.willSetStateTo("done"));
295+
296+
ConfigMapConfigProperties.Source sourceOne = new ConfigMapConfigProperties.Source(null, namespace,
297+
configMapOneLabels, null, null, null);
298+
ConfigMapConfigProperties.Source sourceTwo = new ConfigMapConfigProperties.Source(null, namespace,
299+
configMapTwoLabels, null, null, null);
300+
301+
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(),
302+
List.of(sourceOne, sourceTwo), Map.of("one", "1", "two", "2"), true, null, namespace, false, true,
303+
false, RetryProperties.DEFAULT);
304+
305+
CoreV1Api api = new CoreV1Api();
306+
KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api,
307+
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
308+
309+
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment());
310+
List<String> names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList();
311+
312+
// two sources are present, one being empty
313+
assertThat(names).containsExactly("configmap.two.default", SourceData.EMPTY_SOURCE_NAME_ON_ERROR);
314+
315+
assertThat(output).contains("failure in reading labeled sources");
316+
assertThat(output).contains("failure in reading named sources");
317+
318+
}
319+
320+
/**
321+
* <pre>
322+
* there are two sources and we try to read them.
323+
* both fail.
324+
* </pre>
325+
*/
326+
@Test
327+
void labeledTwoConfigMapsBothFail(CapturedOutput output) {
328+
329+
Map<String, String> configMapOneLabels = Map.of("one", "1");
330+
Map<String, String> configMapTwoLabels = Map.of("two", "2");
331+
332+
String namespace = "default";
333+
String path = "/api/v1/namespaces/default/configmaps";
334+
335+
// one for 'application' named configmap and two for the labeled configmaps
336+
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))
337+
.inScenario("started")
338+
.willSetStateTo("first"));
339+
340+
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))
341+
.inScenario("started")
342+
.whenScenarioStateIs("first")
343+
.willSetStateTo("second"));
344+
345+
stubFor(get(path).willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))
346+
.inScenario("started")
347+
.whenScenarioStateIs("second")
348+
.willSetStateTo("done"));
349+
350+
ConfigMapConfigProperties.Source sourceOne = new ConfigMapConfigProperties.Source(null, namespace,
351+
configMapOneLabels, null, null, null);
352+
ConfigMapConfigProperties.Source sourceTwo = new ConfigMapConfigProperties.Source(null, namespace,
353+
configMapTwoLabels, null, null, null);
354+
355+
ConfigMapConfigProperties configMapConfigProperties = new ConfigMapConfigProperties(true, List.of(),
356+
List.of(sourceOne, sourceTwo), Map.of("one", "1", "two", "2"), true, null, namespace, false, true,
357+
false, RetryProperties.DEFAULT);
358+
359+
CoreV1Api api = new CoreV1Api();
360+
KubernetesClientConfigMapPropertySourceLocator locator = new KubernetesClientConfigMapPropertySourceLocator(api,
361+
configMapConfigProperties, new KubernetesNamespaceProvider(new MockEnvironment()));
362+
363+
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(new MockEnvironment());
364+
List<String> names = propertySource.getPropertySources().stream().map(PropertySource::getName).toList();
365+
366+
// all 3 sources ('application' named source, and two labeled sources)
367+
assertThat(names).containsExactly(SourceData.EMPTY_SOURCE_NAME_ON_ERROR);
368+
369+
assertThat(output).contains("failure in reading labeled sources");
370+
assertThat(output).contains("failure in reading named sources");
371+
372+
}
373+
374+
}

0 commit comments

Comments
 (0)