refactor k8s discovery server integration tests (#1877)

* started

Signed-off-by: wind57 <eugen.rabii@gmail.com>

* dirty

Signed-off-by: wind57 <eugen.rabii@gmail.com>

* fix

Signed-off-by: wind57 <eugen.rabii@gmail.com>

* fix test

Signed-off-by: wind57 <eugen.rabii@gmail.com>

* fix test

Signed-off-by: wind57 <eugen.rabii@gmail.com>

---------

Signed-off-by: wind57 <eugen.rabii@gmail.com>
This commit is contained in:
erabii
2025-02-20 02:57:14 +02:00
committed by GitHub
parent f1c7e9d8ba
commit cf728701fd
17 changed files with 389 additions and 643 deletions

View File

@@ -11,6 +11,11 @@
<artifactId>spring-cloud-kubernetes-k8s-client-discovery-server</artifactId>
<properties>
<spring-boot.repackage.skip>true</spring-boot.repackage.skip>
<spring-boot.build-image.skip>true</spring-boot.build-image.skip>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -43,10 +48,6 @@
<directory>../src/main/resources</directory>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2013-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.kubernetes.discoveryclient.it;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* @author wind57
*/
@SpringBootApplication
@EnableScheduling
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}

View File

@@ -35,12 +35,12 @@ class HeartbeatListener implements ApplicationListener<HeartbeatEvent> {
private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(HeartbeatListener.class));
AtomicReference<List<EndpointNameAndNamespace>> state = new AtomicReference<>(List.of());
public final AtomicReference<List<EndpointNameAndNamespace>> state = new AtomicReference<>(List.of());
@Override
@SuppressWarnings("unchecked")
public void onApplicationEvent(HeartbeatEvent event) {
LOG.info("received heartbeat event");
LOG.info("received heartbeat event in listener");
List<EndpointNameAndNamespace> state = (List<EndpointNameAndNamespace>) event.getValue();
this.state.set(state);
LOG.info("state received : " + state);

View File

@@ -1,67 +0,0 @@
/*
* Copyright 2013-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.kubernetes.discoveryclient.it;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Ryan Baxter
*/
@SpringBootApplication
@EnableScheduling
@RestController
class KubernetesDiscoveryClientApplicationIt {
private final DiscoveryClient discoveryClient;
private final HeartbeatListener heartbeatListener;
KubernetesDiscoveryClientApplicationIt(DiscoveryClient discoveryClient, HeartbeatListener heartbeatListener) {
this.discoveryClient = discoveryClient;
this.heartbeatListener = heartbeatListener;
}
public static void main(String[] args) {
SpringApplication.run(KubernetesDiscoveryClientApplicationIt.class, args);
}
@GetMapping("/services")
List<String> services() {
return discoveryClient.getServices();
}
@GetMapping("/service/{serviceId}")
List<ServiceInstance> service(@PathVariable String serviceId) {
return discoveryClient.getInstances(serviceId);
}
@GetMapping("/state")
List<EndpointNameAndNamespace> state() {
return heartbeatListener.state.get();
}
}

View File

@@ -1,17 +0,0 @@
server:
servlet:
context-path: /discoveryclient-it
spring:
cloud:
kubernetes:
discovery:
discoveryServerUrl: http://spring-cloud-kubernetes-discoveryserver
management:
endpoint:
health:
show-details: always
endpoints:
web:
exposure:
include: "*"

View File

@@ -1,149 +0,0 @@
/*
* Copyright 2013-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.kubernetes.discoveryclient.it;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Objects;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.Condition;
import org.testcontainers.k3s.K3sContainer;
import reactor.netty.http.client.HttpClient;
import reactor.util.retry.Retry;
import reactor.util.retry.RetryBackoffSpec;
import org.springframework.boot.test.json.BasicJsonTester;
import org.springframework.cloud.kubernetes.integration.tests.commons.Commons;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
/**
* @author mbialkowski1
*/
final class DiscoveryClientFilterNamespaceDelegate {
private DiscoveryClientFilterNamespaceDelegate() {
}
private static final BasicJsonTester BASIC_JSON_TESTER = new BasicJsonTester(
DiscoveryClientFilterNamespaceDelegate.class);
static void testNamespaceDiscoveryClient(K3sContainer container) {
testLoadBalancer();
testHealth();
testForHeartbeat(container);
}
private static void testLoadBalancer() {
WebClient.Builder builder = builder();
WebClient serviceClient = builder.baseUrl("http://localhost:80/discoveryclient-it/services").build();
String result = serviceClient.method(HttpMethod.GET)
.retrieve()
.bodyToMono(String.class)
.retryWhen(retrySpec())
.block();
Assertions.assertThat(BASIC_JSON_TESTER.from(result))
.extractingJsonPathArrayValue("$")
.contains("service-wiremock");
// ServiceInstance
WebClient serviceInstanceClient = builder
.baseUrl("http://localhost:80/discoveryclient-it/service/service-wiremock")
.build();
String serviceInstances = serviceInstanceClient.method(HttpMethod.GET)
.retrieve()
.bodyToMono(String.class)
.retryWhen(retrySpec())
.block();
Assertions.assertThat(BASIC_JSON_TESTER.from(serviceInstances))
.extractingJsonPathStringValue("$.[0].serviceId")
.isEqualTo("service-wiremock");
Assertions.assertThat(BASIC_JSON_TESTER.from(serviceInstances))
.extractingJsonPathStringValue("$.[0].namespace")
.isEqualTo("left");
}
private static void testHealth() {
WebClient.Builder builder = builder();
WebClient serviceClient = builder.baseUrl("http://localhost:80/discoveryclient-it/actuator/health").build();
String health = serviceClient.method(HttpMethod.GET)
.retrieve()
.bodyToMono(String.class)
.retryWhen(retrySpec())
.block();
Assertions.assertThat(BASIC_JSON_TESTER.from(health))
.extractingJsonPathStringValue("$.components.discoveryComposite.status")
.isEqualTo("UP");
}
private static void testForHeartbeat(K3sContainer container) {
// 1. logs from discovery server
Commons.waitForLogStatement("using delay : 3000", container, "spring-cloud-kubernetes-discoveryserver");
Commons.waitForLogStatement("received heartbeat event", container, "spring-cloud-kubernetes-discoveryserver");
Commons.waitForLogStatement("state received :", container, "spring-cloud-kubernetes-discoveryserver");
// 2. logs from discovery client
Commons.waitForLogStatement("using delay : 3000", container,
"spring-cloud-kubernetes-k8s-client-discovery-server");
Commons.waitForLogStatement("state received : ", container,
"spring-cloud-kubernetes-k8s-client-discovery-server");
// 3. heartbeat listener message
WebClient.Builder builder = builder();
WebClient client = builder.baseUrl("http://localhost:80/discoveryclient-it/state").build();
String result = client.method(HttpMethod.GET)
.retrieve()
.bodyToMono(String.class)
.retryWhen(retrySpec())
.block();
Condition<LinkedHashMap<String, String>> wireMockService = new Condition<>(
map -> map.entrySet().stream().anyMatch(en -> en.getValue().contains("service-wiremock-deployment")),
"");
Condition<LinkedHashMap<String, String>> discoveryServerService = new Condition<>(map -> map.entrySet()
.stream()
.anyMatch(en -> en.getValue().contains("spring-cloud-kubernetes-k8s-client-discovery-server")), "");
Assertions.assertThat(BASIC_JSON_TESTER.from(result))
.<LinkedHashMap<String, String>>extractingJsonPathArrayValue("$.[*]")
.areAtLeastOne(wireMockService);
Assertions.assertThat(BASIC_JSON_TESTER.from(result))
.<LinkedHashMap<String, String>>extractingJsonPathArrayValue("$.[*]")
.areAtLeastOne(discoveryServerService);
}
private static WebClient.Builder builder() {
return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create()));
}
private static RetryBackoffSpec retrySpec() {
return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull);
}
}

View File

@@ -1,321 +0,0 @@
/*
* Copyright 2013-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.kubernetes.discoveryclient.it;
import java.time.Duration;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import io.kubernetes.client.openapi.apis.RbacAuthorizationV1Api;
import io.kubernetes.client.openapi.models.V1ClusterRoleBinding;
import io.kubernetes.client.openapi.models.V1Deployment;
import io.kubernetes.client.openapi.models.V1Ingress;
import io.kubernetes.client.openapi.models.V1Service;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.testcontainers.k3s.K3sContainer;
import reactor.netty.http.client.HttpClient;
import reactor.util.retry.Retry;
import reactor.util.retry.RetryBackoffSpec;
import org.springframework.boot.test.json.BasicJsonTester;
import org.springframework.cloud.kubernetes.integration.tests.commons.Commons;
import org.springframework.cloud.kubernetes.integration.tests.commons.Images;
import org.springframework.cloud.kubernetes.integration.tests.commons.Phase;
import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import static org.springframework.cloud.kubernetes.discoveryclient.it.DiscoveryClientFilterNamespaceDelegate.testNamespaceDiscoveryClient;
import static org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util.patchWithReplace;
/**
* @author Ryan Baxter
*/
class DiscoveryClientIT {
private static final String BODY_ONE = """
{
"spec": {
"template": {
"spec": {
"containers": [{
"name": "spring-cloud-kubernetes-k8s-client-discovery-server",
"image": "image_name_here",
"env": [
{
"name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_0",
"value": "left"
},
{
"name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_CATALOGSERVICESWATCHDELAY",
"value": "3000"
},
{
"name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_DISCOVERY",
"value": "DEBUG"
},
{
"name": "SPRING_CLOUD_KUBERNETES_HTTP_DISCOVERY_CATALOG_WATCHER_ENABLED",
"value": "TRUE"
}
]
}]
}
}
}
}
""";
private static final String BODY_TWO = """
{
"spec": {
"template": {
"spec": {
"containers": [{
"name": "spring-cloud-kubernetes-k8s-client-discovery-server",
"image": "image_name_here",
"env": [
{
"name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_ALL_NAMESPACES",
"value": "TRUE"
},
{
"name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_DISCOVERYSERVER",
"value": "DEBUG"
},
{
"name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_CATALOGSERVICESWATCHDELAY",
"value": "3000"
},
{
"name": "SPRING_CLOUD_KUBERNETES_HTTP_DISCOVERY_CATALOG_WATCHER_ENABLED",
"value": "TRUE"
}
]
}]
}
}
}
}
""";
private static final Map<String, String> POD_LABELS = Map.of("app",
"spring-cloud-kubernetes-k8s-client-discovery-server");
private static final Map<String, String> POD_LABELS_DISCOVERY = Map.of("app",
"spring-cloud-kubernetes-discoveryserver");
private static final BasicJsonTester BASIC_JSON_TESTER = new BasicJsonTester(DiscoveryClientIT.class);
private static final String DISCOVERY_SERVER_APP_NAME = "spring-cloud-kubernetes-discoveryserver";
private static final String SPRING_CLOUD_K8S_DISCOVERY_CLIENT_APP_NAME = "spring-cloud-kubernetes-k8s-client-discovery-server";
private static final String NAMESPACE = "default";
private static final String NAMESPACE_LEFT = "left";
private static final String NAMESPACE_RIGHT = "right";
private static final K3sContainer K3S = Commons.container();
private static Util util;
private static RbacAuthorizationV1Api rbacApi;
private static V1ClusterRoleBinding clusterRoleBinding;
@BeforeAll
static void beforeAll() throws Exception {
K3S.start();
Commons.validateImage(DISCOVERY_SERVER_APP_NAME, K3S);
Commons.loadSpringCloudKubernetesImage(DISCOVERY_SERVER_APP_NAME, K3S);
Commons.validateImage(SPRING_CLOUD_K8S_DISCOVERY_CLIENT_APP_NAME, K3S);
Commons.loadSpringCloudKubernetesImage(SPRING_CLOUD_K8S_DISCOVERY_CLIENT_APP_NAME, K3S);
Images.loadWiremock(K3S);
util = new Util(K3S);
rbacApi = new RbacAuthorizationV1Api();
util.setUp(NAMESPACE);
util.createNamespace(NAMESPACE_LEFT);
util.createNamespace(NAMESPACE_RIGHT);
clusterRoleBinding = (V1ClusterRoleBinding) util
.yaml("namespace-filter/cluster-admin-serviceaccount-role.yaml");
rbacApi.createClusterRoleBinding(clusterRoleBinding, null, null, null, null);
util.wiremock(NAMESPACE_LEFT, "/wiremock-" + NAMESPACE_LEFT, Phase.CREATE, false);
util.wiremock(NAMESPACE_RIGHT, "/wiremock-" + NAMESPACE_RIGHT, Phase.CREATE, false);
discoveryServer(Phase.CREATE);
}
@AfterAll
static void afterAll() throws Exception {
rbacApi.deleteClusterRoleBinding(clusterRoleBinding.getMetadata().getName(), null, null, null, null, null,
null);
util.wiremock(NAMESPACE_LEFT, "/wiremock-" + NAMESPACE_LEFT, Phase.DELETE, false);
util.wiremock(NAMESPACE_RIGHT, "/wiremock-" + NAMESPACE_RIGHT, Phase.DELETE, false);
util.deleteNamespace(NAMESPACE_LEFT);
util.deleteNamespace(NAMESPACE_RIGHT);
discoveryServer(Phase.DELETE);
discoveryClient(Phase.DELETE);
}
@Test
void testDiscoveryClient() {
discoveryClient(Phase.CREATE);
testLoadBalancer();
testHealth();
patchForAllNamespaces("docker.io/springcloud/spring-cloud-kubernetes-discoveryserver:" + Commons.pomVersion(),
"spring-cloud-kubernetes-discoveryserver-deployment", NAMESPACE);
patchForNamespaceFilter(
"docker.io/springcloud/spring-cloud-kubernetes-k8s-client-discovery-server:" + Commons.pomVersion(),
"spring-cloud-kubernetes-k8s-client-discovery-server-deployment", NAMESPACE);
testNamespaceDiscoveryClient(K3S);
}
private void testLoadBalancer() {
WebClient.Builder builder = builder();
WebClient serviceClient = builder.baseUrl("http://localhost:80/discoveryclient-it/services").build();
String result = serviceClient.method(HttpMethod.GET)
.retrieve()
.bodyToMono(String.class)
.retryWhen(retrySpec())
.block();
Assertions.assertThat(BASIC_JSON_TESTER.from(result))
.extractingJsonPathArrayValue("$")
.contains("spring-cloud-kubernetes-discoveryserver");
// since 'spring.cloud.kubernetes.http.discovery.client.catalog.watcher.enabled'
// is false by default, we will not receive any heartbeat events,
// simply because there are no beans registered to provide that to us.
// We assert this by doing a call to our internal /state
// endpoint, waiting 10 seconds and doing it again. Since the watch delay is set
// to 3 seconds, if there would be proper events,
// we would get a result that is different from '[]'.
WebClient.Builder stateBuilder = builder();
WebClient client = stateBuilder.baseUrl("http://localhost:80/discoveryclient-it/state").build();
String stateResult = client.method(HttpMethod.GET)
.retrieve()
.bodyToMono(String.class)
.retryWhen(retrySpec())
.block();
Assertions.assertThat(BASIC_JSON_TESTER.from(stateResult)).isEqualTo("[]");
try {
Thread.sleep(TimeUnit.SECONDS.toMillis(10));
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
String stateResultAfter10Seconds = client.method(HttpMethod.GET)
.retrieve()
.bodyToMono(String.class)
.retryWhen(retrySpec())
.block();
Assertions.assertThat(BASIC_JSON_TESTER.from(stateResultAfter10Seconds)).isEqualTo("[]");
}
void testHealth() {
WebClient.Builder clientBuilder = builder();
WebClient.Builder serverBuilder = builder();
WebClient client = clientBuilder.baseUrl("http://localhost:80/discoveryclient-it/actuator/health").build();
WebClient server = serverBuilder.baseUrl("http://localhost:80/actuator/health").build();
String clientHealth = client.method(HttpMethod.GET)
.retrieve()
.bodyToMono(String.class)
.retryWhen(retrySpec())
.block();
String serverHealth = server.method(HttpMethod.GET)
.retrieve()
.bodyToMono(String.class)
.retryWhen(retrySpec())
.block();
Assertions.assertThat(BASIC_JSON_TESTER.from(clientHealth))
.extractingJsonPathStringValue("$.components.discoveryComposite.status")
.isEqualTo("UP");
Assertions.assertThat(BASIC_JSON_TESTER.from(serverHealth))
.extractingJsonPathStringValue("$.components.kubernetes.status")
.isEqualTo("UP");
}
private static void discoveryClient(Phase phase) {
V1Deployment deployment = (V1Deployment) util
.yaml("client/spring-cloud-kubernetes-discoveryclient-it-deployment.yaml");
V1Service service = (V1Service) util.yaml("client/spring-cloud-kubernetes-discoveryclient-it-service.yaml");
V1Ingress ingress = (V1Ingress) util.yaml("ingress.yaml");
if (phase.equals(Phase.CREATE)) {
util.createAndWait(NAMESPACE, null, deployment, service, ingress, true);
}
else {
util.deleteAndWait(NAMESPACE, deployment, service, ingress);
}
}
private static void discoveryServer(Phase phase) {
V1Deployment deployment = (V1Deployment) util
.yaml("server/spring-cloud-kubernetes-discoveryserver-deployment.yaml");
V1Service service = (V1Service) util.yaml("server/spring-cloud-kubernetes-discoveryserver-service.yaml");
if (phase.equals(Phase.CREATE)) {
util.createAndWait(NAMESPACE, null, deployment, service, null, true);
}
else {
util.deleteAndWait(NAMESPACE, deployment, service, null);
}
}
private WebClient.Builder builder() {
return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create()));
}
private RetryBackoffSpec retrySpec() {
return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull);
}
static void patchForNamespaceFilter(String image, String deploymentName, String namespace) {
patchWithReplace(image, deploymentName, namespace, BODY_ONE, POD_LABELS);
}
static void patchForAllNamespaces(String image, String deploymentName, String namespace) {
patchWithReplace(image, deploymentName, namespace, BODY_TWO, POD_LABELS_DISCOVERY);
}
}

View File

@@ -0,0 +1,150 @@
/*
* Copyright 2013-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.kubernetes.discoveryclient.it;
import java.io.IOException;
import java.io.StringReader;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Set;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.openapi.apis.RbacAuthorizationV1Api;
import io.kubernetes.client.openapi.models.V1ClusterRoleBinding;
import io.kubernetes.client.openapi.models.V1Deployment;
import io.kubernetes.client.openapi.models.V1Service;
import io.kubernetes.client.util.Config;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.extension.ExtendWith;
import org.testcontainers.k3s.K3sContainer;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.cloud.kubernetes.integration.tests.commons.Commons;
import org.springframework.cloud.kubernetes.integration.tests.commons.Phase;
import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(OutputCaptureExtension.class)
abstract class DiscoveryServerClientBase {
private static final String DISCOVERY_SERVER_LABEL = "spring-cloud-kubernetes-discoveryserver";
protected static final String NAMESPACE = "default";
protected static final String NAMESPACE_LEFT = "left";
protected static final String NAMESPACE_RIGHT = "right";
protected static final String DISCOVERY_SERVER_APP_NAME = "spring-cloud-kubernetes-discoveryserver";
protected static final K3sContainer K3S = Commons.container();
protected static Util util;
@BeforeAll
protected static void beforeAll() {
K3S.start();
util = new Util(K3S);
}
protected static ApiClient apiClient() {
String kubeConfigYaml = K3S.getKubeConfigYaml();
ApiClient client;
try {
client = Config.fromConfig(new StringReader(kubeConfigYaml));
}
catch (IOException e) {
throw new RuntimeException(e);
}
return new CoreV1Api(client).getApiClient();
}
protected static KubernetesDiscoveryProperties discoveryProperties(Set<String> namespaces) {
KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(true, null, true,
null, true, "port.", true, true);
return new KubernetesDiscoveryProperties(true, false, namespaces, true, 60, false, null, Set.of(443, 8443),
Map.of(), null, metadata, 0, false, true, "http://localhost:32321");
}
protected static void discoveryServer(Phase phase) {
V1Deployment deployment = (V1Deployment) util.yaml("manifests/discoveryserver-deployment.yaml");
V1Service service = (V1Service) util.yaml("manifests/discoveryserver-service.yaml");
if (phase.equals(Phase.CREATE)) {
util.createAndWait(NAMESPACE, null, deployment, service, null, true);
}
else {
util.deleteAndWait(NAMESPACE, deployment, service, null);
}
}
protected static void serviceAccount(Phase phase) {
try {
V1ClusterRoleBinding clusterRoleBinding = (V1ClusterRoleBinding) util.yaml("manifests/cluster-role.yaml");
RbacAuthorizationV1Api rbacApi = new RbacAuthorizationV1Api();
if (phase == Phase.CREATE) {
rbacApi.createClusterRoleBinding(clusterRoleBinding, null, null, null, null);
}
else {
rbacApi.deleteClusterRoleBinding(clusterRoleBinding.getMetadata().getName(), null, null, null, null,
null, null);
}
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
protected static void testHeartBeat(HeartbeatListener heartbeatListener, CapturedOutput output) {
// 1. logs from discovery server
Commons.waitForLogStatement("using delay : 3000", K3S, DISCOVERY_SERVER_LABEL);
Commons.waitForLogStatement("received heartbeat event", K3S, DISCOVERY_SERVER_LABEL);
Commons.waitForLogStatement("state received :", K3S, DISCOVERY_SERVER_LABEL);
// 2. logs from discovery client
assertThat(output.getOut()).contains("using delay : 3000");
assertThat(output.getOut()).contains("state received : ");
assertThat(output.getOut()).contains("received heartbeat event in listener");
// 3. heartbeat listener message
List<EndpointNameAndNamespace> result = heartbeatListener.state.get();
Awaitility.await()
.atMost(Duration.ofSeconds(60))
.pollInterval(Duration.ofSeconds(1))
.until(() -> result.size() == 2);
List<String> namespaces = result.stream().map(EndpointNameAndNamespace::namespace).toList();
assertThat(namespaces).containsExactlyInAnyOrder(NAMESPACE_LEFT, NAMESPACE_RIGHT);
List<String> endpointNames = result.stream().map(EndpointNameAndNamespace::endpointName).toList();
assertThat(endpointNames.get(0)).contains("service-wiremock-deployment");
assertThat(endpointNames.get(1)).contains("service-wiremock-deployment");
}
}

View File

@@ -0,0 +1,166 @@
/*
* Copyright 2013-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.kubernetes.discoveryclient.it;
import java.util.List;
import java.util.Set;
import io.kubernetes.client.openapi.ApiClient;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.cloud.kubernetes.integration.tests.commons.Commons;
import org.springframework.cloud.kubernetes.integration.tests.commons.Images;
import org.springframework.cloud.kubernetes.integration.tests.commons.Phase;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.test.context.TestPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author wind57
*/
@SpringBootTest(classes = { App.class, DiscoveryServerClientIT.TestConfig.class },
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(properties = { "spring.main.cloud-platform=kubernetes",
"logging.level.org.springframework.cloud.kubernetes.discovery=debug",
"spring.cloud.kubernetes.discovery.catalogServicesWatchDelay=3000",
"spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=true" })
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class DiscoveryServerClientIT extends DiscoveryServerClientBase {
@Autowired
private DiscoveryClient discoveryClient;
@Autowired
private ReactiveDiscoveryClient reactiveDiscoveryClient;
@Autowired
private HeartbeatListener heartbeatListener;
@BeforeAll
static void beforeAllLocal() throws Exception {
util.createNamespace(NAMESPACE_LEFT);
util.createNamespace(NAMESPACE_RIGHT);
Commons.validateImage(DISCOVERY_SERVER_APP_NAME, K3S);
Commons.loadSpringCloudKubernetesImage(DISCOVERY_SERVER_APP_NAME, K3S);
util.setUp(NAMESPACE);
serviceAccount(Phase.CREATE);
discoveryServer(Phase.CREATE);
Images.loadWiremock(K3S);
util.wiremock(NAMESPACE_LEFT, "/wiremock-" + NAMESPACE_LEFT, Phase.CREATE, false);
util.wiremock(NAMESPACE_RIGHT, "/wiremock-" + NAMESPACE_RIGHT, Phase.CREATE, false);
}
@AfterAll
static void afterAllLocal() {
serviceAccount(Phase.DELETE);
discoveryServer(Phase.DELETE);
util.wiremock(NAMESPACE_LEFT, "/wiremock-" + NAMESPACE_LEFT, Phase.DELETE, false);
util.wiremock(NAMESPACE_RIGHT, "/wiremock-" + NAMESPACE_RIGHT, Phase.DELETE, false);
util.deleteNamespace(NAMESPACE_LEFT);
util.deleteNamespace(NAMESPACE_RIGHT);
}
/**
* <pre>
* - searching is enabled in two namespaces : left and right
* </pre>
*/
@Test
@Order(1)
void testBlocking(CapturedOutput output) {
List<String> services = reactiveDiscoveryClient.getServices().collectList().block();
assertThat(services).hasSize(1);
assertThat(services).contains("service-wiremock");
List<ServiceInstance> serviceInstances = reactiveDiscoveryClient.getInstances("service-wiremock")
.collectList()
.block();
List<DefaultKubernetesServiceInstance> defaultKubernetesServiceInstances = serviceInstances.stream()
.map(x -> (DefaultKubernetesServiceInstance) x)
.toList();
assertThat(defaultKubernetesServiceInstances).hasSize(2);
List<String> namespaces = defaultKubernetesServiceInstances.stream()
.map(DefaultKubernetesServiceInstance::getNamespace)
.toList();
assertThat(namespaces).containsExactlyInAnyOrder(NAMESPACE_LEFT, NAMESPACE_RIGHT);
testHeartBeat(heartbeatListener, output);
}
/**
* <pre>
* - searching is enabled in two namespaces : left and right
* </pre>
*/
@Test
@Order(2)
void testReactive() {
List<String> services = discoveryClient.getServices();
assertThat(services).hasSize(1);
assertThat(services).contains("service-wiremock");
List<ServiceInstance> serviceInstances = discoveryClient.getInstances("service-wiremock");
List<DefaultKubernetesServiceInstance> defaultKubernetesServiceInstances = serviceInstances.stream()
.map(x -> (DefaultKubernetesServiceInstance) x)
.toList();
assertThat(defaultKubernetesServiceInstances).hasSize(2);
List<String> namespaces = defaultKubernetesServiceInstances.stream()
.map(DefaultKubernetesServiceInstance::getNamespace)
.toList();
assertThat(namespaces).containsExactlyInAnyOrder(NAMESPACE_LEFT, NAMESPACE_RIGHT);
}
@TestConfiguration
static class TestConfig {
@Bean
@Primary
ApiClient client() {
return apiClient();
}
@Bean
@Primary
KubernetesDiscoveryProperties kubernetesDiscoveryProperties() {
return discoveryProperties(Set.of(NAMESPACE_LEFT, NAMESPACE_RIGHT));
}
}
}

View File

@@ -1,37 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-cloud-kubernetes-k8s-client-discovery-server-deployment
spec:
selector:
matchLabels:
app: spring-cloud-kubernetes-k8s-client-discovery-server
template:
metadata:
labels:
app: spring-cloud-kubernetes-k8s-client-discovery-server
spec:
serviceAccountName: spring-cloud-kubernetes-serviceaccount
containers:
- name: spring-cloud-kubernetes-k8s-client-discovery-server
image: docker.io/springcloud/spring-cloud-kubernetes-k8s-client-discovery-server
imagePullPolicy: IfNotPresent
readinessProbe:
httpGet:
port: 8080
path: /discoveryclient-it/actuator/health/readiness
initialDelaySeconds: 10
periodSeconds: 2
failureThreshold: 3
livenessProbe:
httpGet:
port: 8080
path: /discoveryclient-it/actuator/health/liveness
initialDelaySeconds: 10
periodSeconds: 2
failureThreshold: 3
env:
- name: SPRING_CLOUD_KUBERNETES_DISCOVERY_CATALOGSERVICESWATCHDELAY
value: "3000"
ports:
- containerPort: 8080

View File

@@ -1,14 +0,0 @@
apiVersion: v1
kind: Service
metadata:
labels:
app: spring-cloud-kubernetes-k8s-client-discovery-server
name: spring-cloud-kubernetes-k8s-client-discovery-server
spec:
ports:
- name: http
port: 8080
targetPort: 8080
selector:
app: spring-cloud-kubernetes-k8s-client-discovery-server
type: ClusterIP

View File

@@ -1,24 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: it-ingress
namespace: default
spec:
rules:
- http:
paths:
- path: /discoveryclient-it
pathType: Prefix
backend:
service:
name: spring-cloud-kubernetes-k8s-client-discovery-server
port:
number: 8080
- path: /
pathType: Prefix
backend:
service:
name: spring-cloud-kubernetes-discoveryserver
port:
number: 80

View File

@@ -18,20 +18,28 @@ spec:
imagePullPolicy: IfNotPresent
readinessProbe:
httpGet:
port: 8761
port: 32321
path: /actuator/health/readiness
livenessProbe:
httpGet:
port: 8761
port: 32321
path: /actuator/health/liveness
env:
- name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_DISCOVERYSERVER
value: "DEBUG"
- name: SPRING_CLOUD_KUBERNETES_DISCOVERY_CATALOGSERVICESWATCHDELAY
value: "3000"
- name: SPRING_CLOUD_KUBERNETES_HTTP_DISCOVERY_CATALOG_WATCHER_ENABLED
value: "TRUE"
- name: MANAGEMENT_ENDPOINT_HEALTH_SHOWCOMPONENTS
value: "ALWAYS"
- name: MANAGEMENT_ENDPOINT_HEALTH_SHOWDETAILS
value: "ALWAYS"
- name: SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_0
value: left
- name: SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_1
value: right
- name: SERVER_PORT
value: "32321"
ports:
- containerPort: 8761
- containerPort: 32321

View File

@@ -7,8 +7,9 @@ metadata:
spec:
ports:
- name: http
port: 80
targetPort: 8761
port: 32321
targetPort: 32321
nodePort: 32321
selector:
app: spring-cloud-kubernetes-discoveryserver
type: ClusterIP
type: NodePort

View File

@@ -28,7 +28,7 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.web.server.LocalManagementPort;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.cloud.kubernetes.integration.tests.commons.Images;
import org.springframework.cloud.kubernetes.integration.tests.commons.Phase;
@@ -55,7 +55,7 @@ class KubernetesClientReactiveIT extends KubernetesClientDiscoveryBase {
private int port;
@Autowired
private DiscoveryClient discoveryClient;
private ReactiveDiscoveryClient discoveryClient;
@BeforeEach
void beforeEach() {

View File

@@ -24,6 +24,7 @@ import org.springframework.boot.test.json.BasicJsonTester;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance;
import org.springframework.http.HttpMethod;
import org.springframework.web.reactive.function.client.WebClient;
@@ -137,4 +138,18 @@ final class TestAssertions {
}
static void assertPodMetadata(ReactiveDiscoveryClient discoveryClient) {
List<ServiceInstance> serviceInstances = discoveryClient.getInstances("service-wiremock").collectList().block();
assertThat(serviceInstances).hasSize(1);
DefaultKubernetesServiceInstance wiremockInstance = (DefaultKubernetesServiceInstance) serviceInstances.get(0);
assertThat(wiremockInstance.getServiceId()).isEqualTo("service-wiremock");
assertThat(wiremockInstance.getInstanceId()).isNotNull();
assertThat(wiremockInstance.getHost()).isNotNull();
assertThat(wiremockInstance.getMetadata()).isEqualTo(Map.of("k8s_namespace", "default", "type", "ClusterIP",
"port.http", "8080", "app", "service-wiremock"));
}
}