Skip to content

Commit

Permalink
Cleanup k8s client loadbalancer part 2 (#1613)
Browse files Browse the repository at this point in the history
* test

* fix @nested tests not running

* trigger again

* Add renovate.json

* Delete renovate.json

* Delete delme.sh

* dirty

* first

* checkstyle

* dirty

* dirty

* add tests

* fix test

* fix tests

* checkstyle

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
  • Loading branch information
wind57 and renovate[bot] authored Apr 3, 2024
1 parent 17471a1 commit 0acf32f
Show file tree
Hide file tree
Showing 18 changed files with 1,349 additions and 368 deletions.
18 changes: 18 additions & 0 deletions spring-cloud-kubernetes-client-loadbalancer/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,20 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.mokito</groupId>
<artifactId>mockito-core</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock-standalone</artifactId>
Expand All @@ -51,6 +64,11 @@
<artifactId>spring-boot-starter-web</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-test-support</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* 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.client.loadbalancer.it;

import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import io.kubernetes.client.openapi.JSON;
import io.kubernetes.client.openapi.models.CoreV1EndpointPortBuilder;
import io.kubernetes.client.openapi.models.V1EndpointAddressBuilder;
import io.kubernetes.client.openapi.models.V1EndpointSubsetBuilder;
import io.kubernetes.client.openapi.models.V1Endpoints;
import io.kubernetes.client.openapi.models.V1EndpointsBuilder;
import io.kubernetes.client.openapi.models.V1EndpointsList;
import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder;
import io.kubernetes.client.openapi.models.V1Service;
import io.kubernetes.client.openapi.models.V1ServiceBuilder;
import io.kubernetes.client.openapi.models.V1ServiceList;
import io.kubernetes.client.openapi.models.V1ServicePortBuilder;
import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.reactive.function.client.WebClient;

/**
* @author wind57
*/
public final class Util {

private Util() {

}

public static V1Service service(String namespace, String name, int port) {
return new V1ServiceBuilder().withNewMetadata().withNamespace(namespace).withName(name).endMetadata()
.withSpec(new V1ServiceSpecBuilder()
.withPorts(new V1ServicePortBuilder().withName("http").withPort(port).build()).build())
.build();
}

public static V1Endpoints endpoints(String namespace, String name, int port, String host) {
return new V1EndpointsBuilder()
.withSubsets(
new V1EndpointSubsetBuilder().withPorts(new CoreV1EndpointPortBuilder().withPort(port).build())
.withAddresses(new V1EndpointAddressBuilder().withIp(host).build()).build())
.withMetadata(new V1ObjectMetaBuilder().withName(name).withNamespace(namespace).build()).build();
}

public static void servicesPodMode(WireMockServer server, V1ServiceList serviceList) {
server.stubFor(WireMock.get(WireMock.urlPathMatching("^/api/v1/services*"))
.willReturn(WireMock.aResponse().withBody(new JSON().serialize(serviceList)).withStatus(200)));
}

public static void servicesServiceMode(WireMockServer server, V1ServiceList serviceList, String serviceName) {
// resourceVersion=0 is passed only from the watcher, so this mocks the
// 'postConstruct' in the KubernetesInformerDiscoveryClient
server.stubFor(WireMock.get(WireMock.urlPathMatching("^/api/v1/services*"))
.withQueryParam("resourceVersion", WireMock.equalTo("0"))
.willReturn(WireMock.aResponse().withBody(new JSON().serialize(serviceList)).withStatus(200)));

server.stubFor(
WireMock.get(WireMock.urlEqualTo("/api/v1/services?fieldSelector=metadata.name%3D" + serviceName))
.willReturn(WireMock.aResponse().withBody(new JSON().serialize(serviceList)).withStatus(200)));
}

public static void endpointsPodMode(WireMockServer server, V1EndpointsList endpointsList) {
server.stubFor(WireMock.get(WireMock.urlPathMatching("^/api/v1/endpoints*"))
.willReturn(WireMock.aResponse().withBody(new JSON().serialize(endpointsList)).withStatus(200)));
}

public static void endpointsServiceMode(WireMockServer server, V1EndpointsList endpointsList,
String endpointsName) {
// resourceVersion=0 is passed only from the watcher, so this mocks the
// 'postConstruct' in the KubernetesInformerDiscoveryClient
server.stubFor(WireMock.get(WireMock.urlPathMatching("^/api/v1/endpoints*"))
.withQueryParam("resourceVersion", WireMock.equalTo("0"))
.willReturn(WireMock.aResponse().withBody(new JSON().serialize(endpointsList)).withStatus(200)));

server.stubFor(WireMock
.get(WireMock.urlEqualTo("/api/v1/endpoints?fieldSelector=metadata.name%3D" + endpointsName))
.willReturn(WireMock.aResponse().withBody(new JSON().serialize(endpointsList)).withStatus(200)));
}

public static void servicesInNamespacePodMode(WireMockServer server, V1ServiceList serviceList, String namespace) {
server.stubFor(WireMock.get(WireMock.urlPathMatching("^/api/v1/namespaces/" + namespace + "/services*"))
.willReturn(WireMock.aResponse().withBody(new JSON().serialize(serviceList)).withStatus(200)));
}

public static void servicesInNamespaceServiceMode(WireMockServer server, V1ServiceList serviceList,
String namespace, String serviceName) {
// resourceVersion=0 is passed only from the watcher, so this mocks the
// 'postConstruct' in the KubernetesInformerDiscoveryClient
server.stubFor(WireMock.get(WireMock.urlPathMatching("^/api/v1/namespaces/" + namespace + "/services*"))
.withQueryParam("resourceVersion", WireMock.equalTo("0"))
.willReturn(WireMock.aResponse().withBody(new JSON().serialize(serviceList)).withStatus(200)));

server.stubFor(WireMock
.get(WireMock.urlEqualTo(
"/api/v1/namespaces/" + namespace + "/services?fieldSelector=metadata.name%3D" + serviceName))
.willReturn(WireMock.aResponse().withBody(new JSON().serialize(serviceList)).withStatus(200)));
}

public static void endpointsInNamespacePodMode(WireMockServer server, V1EndpointsList endpointsList,
String namespace) {
server.stubFor(WireMock.get(WireMock.urlPathMatching("^/api/v1/namespaces/" + namespace + "/endpoints*"))
.willReturn(WireMock.aResponse().withBody(new JSON().serialize(endpointsList)).withStatus(200)));
}

public static void endpointsInNamespaceServiceMode(WireMockServer server, V1EndpointsList endpointsList,
String namespace, String endpointsName) {
// resourceVersion=0 is passed only from the watcher, so this mocks the
// 'postConstruct' in the KubernetesInformerDiscoveryClient
server.stubFor(WireMock.get(WireMock.urlPathMatching("^/api/v1/namespaces/" + namespace + "/endpoints*"))
.withQueryParam("resourceVersion", WireMock.equalTo("0"))
.willReturn(WireMock.aResponse().withBody(new JSON().serialize(endpointsList)).withStatus(200)));

server.stubFor(WireMock
.get(WireMock.urlEqualTo("/api/v1/namespaces/" + namespace + "/endpoints?fieldSelector=metadata.name%3D"
+ endpointsName))
.willReturn(WireMock.aResponse().withBody(new JSON().serialize(endpointsList)).withStatus(200)));
}

@TestConfiguration
public static class LoadBalancerConfiguration {

@Bean
@LoadBalanced
WebClient.Builder client() {
return WebClient.builder();
}

}

@SpringBootApplication
public static class Configuration {

public static void main(String[] args) {
SpringApplication.run(Configuration.class);
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*
* 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.client.loadbalancer.it.mode.pod;

import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.models.V1Endpoints;
import io.kubernetes.client.openapi.models.V1EndpointsList;
import io.kubernetes.client.openapi.models.V1EndpointsListBuilder;
import io.kubernetes.client.openapi.models.V1ListMetaBuilder;
import io.kubernetes.client.openapi.models.V1Service;
import io.kubernetes.client.openapi.models.V1ServiceList;
import io.kubernetes.client.openapi.models.V1ServiceListBuilder;
import io.kubernetes.client.util.ClientBuilder;
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.mockito.MockedStatic;
import org.mockito.Mockito;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.kubernetes.client.KubernetesClientUtils;
import org.springframework.cloud.kubernetes.client.loadbalancer.it.Util;
import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServiceInstanceMapper;
import org.springframework.cloud.loadbalancer.core.CachingServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.DiscoveryClientServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.http.HttpMethod;
import org.springframework.web.reactive.function.client.WebClient;

import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import static org.mockito.Mockito.mockStatic;
import static org.springframework.cloud.kubernetes.client.loadbalancer.it.Util.Configuration;
import static org.springframework.cloud.kubernetes.client.loadbalancer.it.Util.LoadBalancerConfiguration;

/**
* @author wind57
*/
@SpringBootTest(
properties = { "spring.cloud.kubernetes.loadbalancer.mode=POD", "spring.main.cloud-platform=KUBERNETES",
"spring.cloud.kubernetes.discovery.all-namespaces=true" },
classes = { LoadBalancerConfiguration.class, Configuration.class })
class AllNamespacesTest {

private static final String SERVICE_A_URL = "http://service-a";

private static final String SERVICE_B_URL = "http://service-b";

private static final int SERVICE_A_PORT = 8888;

private static final int SERVICE_B_PORT = 8889;

private static WireMockServer wireMockServer;

private static WireMockServer serviceAMockServer;

private static WireMockServer serviceBMockServer;

private static MockedStatic<KubernetesClientUtils> clientUtils;

private static final MockedStatic<KubernetesServiceInstanceMapper> MOCKED_STATIC = Mockito
.mockStatic(KubernetesServiceInstanceMapper.class);

@Autowired
private WebClient.Builder builder;

@Autowired
private ObjectProvider<LoadBalancerClientFactory> loadBalancerClientFactory;

@BeforeAll
static void beforeAll() {

wireMockServer = new WireMockServer(options().dynamicPort());
wireMockServer.start();
WireMock.configureFor("localhost", wireMockServer.port());
mockWatchers();

serviceAMockServer = new WireMockServer(SERVICE_A_PORT);
serviceAMockServer.start();
WireMock.configureFor("localhost", SERVICE_A_PORT);

serviceBMockServer = new WireMockServer(SERVICE_B_PORT);
serviceBMockServer.start();
WireMock.configureFor("localhost", SERVICE_B_PORT);

// we mock host creation so that it becomes something like : localhost:8888
// then wiremock can catch this request, and we can assert for the result
MOCKED_STATIC.when(() -> KubernetesServiceInstanceMapper.createHost("service-a", "a", "cluster.local"))
.thenReturn("localhost");

MOCKED_STATIC.when(() -> KubernetesServiceInstanceMapper.createHost("service-b", "b", "cluster.local"))
.thenReturn("localhost");

ApiClient client = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build();
clientUtils = mockStatic(KubernetesClientUtils.class);
clientUtils.when(KubernetesClientUtils::kubernetesApiClient).thenReturn(client);

}

@AfterAll
static void afterAll() {
wireMockServer.stop();
serviceAMockServer.stop();
serviceBMockServer.stop();
MOCKED_STATIC.close();
clientUtils.close();
}

/**
* <pre>
* - service-a is present in namespace a with exposed port 8888
* - service-b is present in namespace b with exposed port 8889
* - we make two calls to them via the load balancer
* </pre>
*/
@Test
void test() {

serviceAMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/"))
.willReturn(WireMock.aResponse().withBody("service-a-reached").withStatus(200)));

serviceBMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/"))
.willReturn(WireMock.aResponse().withBody("service-b-reached").withStatus(200)));

String serviceAResult = builder.baseUrl(SERVICE_A_URL).build().method(HttpMethod.GET).retrieve()
.bodyToMono(String.class).block();
Assertions.assertThat(serviceAResult).isEqualTo("service-a-reached");

String serviceBResult = builder.baseUrl(SERVICE_B_URL).build().method(HttpMethod.GET).retrieve()
.bodyToMono(String.class).block();
Assertions.assertThat(serviceBResult).isEqualTo("service-b-reached");

CachingServiceInstanceListSupplier supplier = (CachingServiceInstanceListSupplier) loadBalancerClientFactory
.getIfAvailable().getProvider("service-a", ServiceInstanceListSupplier.class).getIfAvailable();
Assertions.assertThat(supplier.getDelegate().getClass())
.isSameAs(DiscoveryClientServiceInstanceListSupplier.class);

}

private static void mockWatchers() {
V1Service serviceA = Util.service("a", "service-a", SERVICE_A_PORT);
V1Service serviceB = Util.service("b", "service-b", SERVICE_B_PORT);
V1ServiceList serviceList = new V1ServiceListBuilder().withKind("V1ServiceList")
.withMetadata(new V1ListMetaBuilder().withResourceVersion("0").build())
.withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata()
.withItems(serviceA, serviceB).build();
Util.servicesPodMode(wireMockServer, serviceList);

V1Endpoints endpointsA = Util.endpoints("a", "service-a", SERVICE_A_PORT, "127.0.0.1");
V1Endpoints endpointsB = Util.endpoints("b", "service-b", SERVICE_B_PORT, "127.0.0.1");
V1EndpointsList endpointsList = new V1EndpointsListBuilder().withKind("V1EndpointsList")
.withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata()
.withItems(endpointsA, endpointsB).build();
Util.endpointsPodMode(wireMockServer, endpointsList);
}

}
Loading

0 comments on commit 0acf32f

Please sign in to comment.