Skip to content

Commit d1e555f

Browse files
committed
started work
Signed-off-by: wind57 <[email protected]>
1 parent 1d5f46d commit d1e555f

File tree

4 files changed

+300
-14
lines changed

4 files changed

+300
-14
lines changed

spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/k8s/client/catalog/watcher/KubernetesClientCatalogWatchIT.java

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -97,20 +97,6 @@ void beforeEach() {
9797
util.busybox(NAMESPACE, Phase.CREATE);
9898
}
9999

100-
/**
101-
* <pre>
102-
* - we deploy a busybox service with 2 replica pods
103-
* - we receive an event from KubernetesCatalogWatcher, assert what is inside it
104-
* - delete the busybox service
105-
* - assert that we receive only spring-cloud-kubernetes-client-catalog-watcher pod
106-
* </pre>
107-
*/
108-
@Test
109-
@Order(1)
110-
void testCatalogWatchWithEndpoints() {
111-
waitForLogStatement("stateGenerator is of type: KubernetesEndpointsCatalogWatch", K3S, APP_NAME);
112-
test();
113-
}
114100

115101
@Test
116102
@Order(2)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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.k8s.client.catalog.watcher.it;
18+
19+
import java.io.IOException;
20+
import java.io.StringReader;
21+
import java.util.Map;
22+
import java.util.Set;
23+
24+
import io.kubernetes.client.openapi.ApiClient;
25+
import io.kubernetes.client.openapi.apis.CoreV1Api;
26+
import io.kubernetes.client.util.Config;
27+
import org.junit.jupiter.api.BeforeAll;
28+
import org.junit.jupiter.api.extension.ExtendWith;
29+
30+
import org.springframework.boot.test.system.OutputCaptureExtension;
31+
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
32+
import org.springframework.cloud.kubernetes.integration.tests.commons.Commons;
33+
import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Util;
34+
import org.springframework.test.context.TestPropertySource;
35+
import org.testcontainers.k3s.K3sContainer;
36+
37+
/**
38+
* @author wind57
39+
*/
40+
41+
@TestPropertySource(
42+
properties = { "spring.main.cloud-platform=kubernetes", "spring.cloud.config.import-check.enabled=false",
43+
"spring.cloud.kubernetes.discovery.catalogServicesWatchDelay=2000",
44+
"spring.cloud.kubernetes.client.namespace=default",
45+
"logging.level.org.springframework.cloud.kubernetes.client.discovery.catalog=DEBUG" })
46+
@ExtendWith(OutputCaptureExtension.class)
47+
abstract class KubernetesClientCatalogWatchBase {
48+
49+
protected static final String NAMESPACE = "default";
50+
51+
protected static final String NAMESPACE_A = "a";
52+
53+
protected static final String NAMESPACE_B = "b";
54+
55+
protected static final K3sContainer K3S = Commons.container();
56+
57+
protected static Util util;
58+
59+
@BeforeAll
60+
protected static void beforeAll() {
61+
K3S.start();
62+
util = new Util(K3S);
63+
}
64+
65+
protected static KubernetesDiscoveryProperties discoveryProperties(boolean useEndpointSlices) {
66+
return new KubernetesDiscoveryProperties(true, false, Set.of(NAMESPACE, NAMESPACE_A), true, 60, false, null,
67+
Set.of(443, 8443), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, useEndpointSlices,
68+
false, null);
69+
}
70+
71+
protected static ApiClient apiClient() {
72+
String kubeConfigYaml = K3S.getKubeConfigYaml();
73+
74+
ApiClient client;
75+
try {
76+
client = Config.fromConfig(new StringReader(kubeConfigYaml));
77+
}
78+
catch (IOException e) {
79+
throw new RuntimeException(e);
80+
}
81+
return new CoreV1Api(client).getApiClient();
82+
}
83+
84+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright 2013-2025 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.k8s.client.catalog.watcher.it;
18+
19+
import java.util.Set;
20+
21+
import io.kubernetes.client.openapi.ApiClient;
22+
import org.junit.jupiter.api.AfterEach;
23+
import org.junit.jupiter.api.BeforeEach;
24+
import org.junit.jupiter.api.Test;
25+
26+
import org.springframework.boot.test.context.SpringBootTest;
27+
import org.springframework.boot.test.context.TestConfiguration;
28+
import org.springframework.boot.test.system.CapturedOutput;
29+
import org.springframework.boot.test.web.server.LocalServerPort;
30+
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
31+
import org.springframework.cloud.kubernetes.integration.tests.commons.Images;
32+
import org.springframework.cloud.kubernetes.integration.tests.commons.Phase;
33+
import org.springframework.cloud.kubernetes.k8s.client.catalog.watcher.Application;
34+
import org.springframework.context.annotation.Bean;
35+
import org.springframework.context.annotation.Primary;
36+
37+
import static org.springframework.cloud.kubernetes.k8s.client.catalog.watcher.it.TestAssertions.assertLogStatement;
38+
import static org.springframework.cloud.kubernetes.k8s.client.catalog.watcher.it.TestAssertions.invokeAndAssert;
39+
40+
@SpringBootTest(classes = { KubernetesClientCatalogWatchEndpointsIT.TestConfig.class, Application.class },
41+
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
42+
class KubernetesClientCatalogWatchEndpointsIT extends KubernetesClientCatalogWatchBase {
43+
44+
@LocalServerPort
45+
private int port;
46+
47+
@BeforeEach
48+
void beforeEach() {
49+
50+
util.createNamespace(NAMESPACE_A);
51+
util.createNamespace(NAMESPACE_B);
52+
53+
Images.loadBusybox(K3S);
54+
55+
util.busybox(NAMESPACE_A, Phase.CREATE);
56+
util.busybox(NAMESPACE_B, Phase.CREATE);
57+
58+
}
59+
60+
@AfterEach
61+
void afterEach() {
62+
// busybox is deleted as part of the assertions, thus not seen here
63+
util.deleteNamespace(NAMESPACE_A);
64+
util.deleteNamespace(NAMESPACE_B);
65+
}
66+
67+
/**
68+
* <pre>
69+
* - we deploy a busybox service with 2 replica pods in two namespaces : a, b
70+
* - we use endpoints
71+
* - we enable namespace filtering for 'default' and 'a'
72+
* - we receive an event from KubernetesCatalogWatcher, assert what is inside it
73+
* - delete the busybox service in 'a' and 'b'
74+
* - assert that we receive an empty response
75+
* </pre>
76+
*/
77+
@Test
78+
void testCatalogWatchWithEndpoints(CapturedOutput output) {
79+
assertLogStatement(output, "stateGenerator is of type: KubernetesEndpointsCatalogWatch");
80+
invokeAndAssert(util, Set.of(NAMESPACE_A, NAMESPACE_B), port, NAMESPACE_A);
81+
}
82+
83+
@TestConfiguration
84+
static class TestConfig {
85+
86+
@Bean
87+
@Primary
88+
ApiClient client() {
89+
return apiClient();
90+
}
91+
92+
@Bean
93+
@Primary
94+
KubernetesDiscoveryProperties kubernetesDiscoveryProperties() {
95+
return discoveryProperties(false);
96+
}
97+
98+
}
99+
100+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright 2013-2025 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.k8s.client.catalog.watcher.it;
18+
19+
import org.assertj.core.api.Assertions;
20+
import org.awaitility.Awaitility;
21+
import org.springframework.boot.test.system.CapturedOutput;
22+
import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace;
23+
import org.springframework.cloud.kubernetes.integration.tests.commons.Phase;
24+
import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Util;
25+
import org.springframework.core.ParameterizedTypeReference;
26+
import org.springframework.core.ResolvableType;
27+
import org.springframework.http.HttpMethod;
28+
import org.springframework.web.reactive.function.client.WebClient;
29+
30+
import java.time.Duration;
31+
import java.util.List;
32+
import java.util.Objects;
33+
import java.util.Set;
34+
35+
import static org.assertj.core.api.Assertions.assertThat;
36+
import static org.awaitility.Awaitility.await;
37+
import static org.springframework.cloud.kubernetes.integration.tests.commons.Commons.builder;
38+
import static org.springframework.cloud.kubernetes.integration.tests.commons.Commons.retrySpec;
39+
40+
/**
41+
* @author wind57
42+
*/
43+
final class TestAssertions {
44+
45+
private TestAssertions() {
46+
47+
}
48+
49+
static void assertLogStatement(CapturedOutput output, String textToAssert) {
50+
Awaitility.await()
51+
.during(Duration.ofSeconds(5))
52+
.pollInterval(Duration.ofMillis(200))
53+
.untilAsserted(() -> Assertions.assertThat(output.getOut()).contains(textToAssert));
54+
}
55+
56+
/**
57+
* the checks are the same for both endpoints and endpoint slices, while the set-up
58+
* for them is different.
59+
*/
60+
@SuppressWarnings("unchecked")
61+
static void invokeAndAssert(Util util, Set<String> namespaces, int port, String assertionNamespace) {
62+
63+
WebClient client = builder().baseUrl("http://localhost:" + port + "/result").build();
64+
EndpointNameAndNamespace[] holder = new EndpointNameAndNamespace[2];
65+
ResolvableType resolvableType = ResolvableType.forClassWithGenerics(List.class, EndpointNameAndNamespace.class);
66+
67+
await().pollInterval(Duration.ofMillis(200)).atMost(Duration.ofSeconds(30)).until(() -> {
68+
List<EndpointNameAndNamespace> result = (List<EndpointNameAndNamespace>) client.method(HttpMethod.GET)
69+
.retrieve()
70+
.bodyToMono(ParameterizedTypeReference.forType(resolvableType.getType()))
71+
.retryWhen(retrySpec())
72+
.block();
73+
74+
if (result != null) {
75+
if (result.size() != 2) {
76+
return false;
77+
}
78+
holder[0] = result.get(0);
79+
holder[1] = result.get(1);
80+
return true;
81+
}
82+
83+
return false;
84+
});
85+
86+
EndpointNameAndNamespace resultOne = holder[0];
87+
EndpointNameAndNamespace resultTwo = holder[1];
88+
89+
assertThat(resultOne).isNotNull();
90+
assertThat(resultTwo).isNotNull();
91+
92+
assertThat(resultOne.endpointName()).contains("busybox");
93+
assertThat(resultTwo.endpointName()).contains("busybox");
94+
95+
assertThat(resultOne.namespace()).isEqualTo(assertionNamespace);
96+
assertThat(resultTwo.namespace()).isEqualTo(assertionNamespace);
97+
98+
namespaces.forEach(namespace -> util.busybox(namespace, Phase.DELETE));
99+
100+
await().pollInterval(Duration.ofSeconds(1)).atMost(Duration.ofSeconds(240)).until(() -> {
101+
List<EndpointNameAndNamespace> result = (List<EndpointNameAndNamespace>) client.method(HttpMethod.GET)
102+
.retrieve()
103+
.bodyToMono(ParameterizedTypeReference.forType(resolvableType.getType()))
104+
.retryWhen(retrySpec())
105+
.block();
106+
107+
// we need to get the event from KubernetesCatalogWatch, but that happens
108+
// on periodic bases. So in order to be sure we got the event we care about
109+
// we wait until there is no entry, which means busybox was deleted
110+
// and KubernetesCatalogWatch received that update.
111+
return Objects.requireNonNull(result).isEmpty();
112+
});
113+
114+
}
115+
116+
}

0 commit comments

Comments
 (0)