Loadbalancer Module Using Kubernetes Java Client (#700)

Loadbalancer implementation using Kuberentes Java Client
This commit is contained in:
Ryan Baxter
2020-12-19 11:19:25 -05:00
committed by GitHub
parent 6fc98726cf
commit 6af0c945dc
34 changed files with 1531 additions and 14 deletions

View File

@@ -2,6 +2,7 @@
This project includes Spring Cloud Load Balancer for load balancing based on Kubernetes Endpoints and provides implementation of load balancer based on Kubernetes Service.
To include it to your project add the following dependency.
====
Fabric8 Implementation
[source,xml]
----
<dependency>
@@ -11,6 +12,17 @@ To include it to your project add the following dependency.
----
====
====
Kubernetes Java Client Implementation
[source,xml]
----
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-client-loadbalancer</artifactId>
</dependency>
----
====
To enable load balancing based on Kubernetes Service name use the following property. Then load balancer would try to call application using address, for example `service-a.default.svc.cluster.local`
====
[source]

View File

@@ -90,6 +90,7 @@
<module>spring-cloud-kubernetes-test-support</module>
<module>spring-cloud-kubernetes-client-autoconfig</module>
<module>spring-cloud-kubernetes-client-config</module>
<module>spring-cloud-kubernetes-client-loadbalancer</module>
<module>spring-cloud-kubernetes-fabric8-autoconfig</module>
<module>spring-cloud-kubernetes-fabric8-config</module>
<module>spring-cloud-kubernetes-fabric8-discovery</module>
@@ -99,6 +100,7 @@
<module>spring-cloud-starter-kubernetes-fabric8-all</module>
<module>spring-cloud-starter-kubernetes-client</module>
<module>spring-cloud-starter-kubernetes-client-config</module>
<module>spring-cloud-starter-kubernetes-client-loadbalancer</module>
<module>spring-cloud-starter-kubernetes-client-all</module>
<module>spring-cloud-kubernetes-examples</module>
<module>spring-cloud-kubernetes-fabric8-leader</module>

View File

@@ -11,11 +11,6 @@
<artifactId>spring-cloud-kubernetes-client-config</artifactId>
<properties>
<wiremock.version>2.26.3</wiremock.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
@@ -81,7 +76,6 @@
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<version>${wiremock.version}</version>
<scope>test</scope>
</dependency>

View File

@@ -37,8 +37,6 @@ import org.springframework.cloud.client.CommonsClientAutoConfiguration;
import org.springframework.cloud.client.ConditionalOnBlockingDiscoveryEnabled;
import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration;
import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration;
import org.springframework.cloud.kubernetes.client.discovery.gson.EndpointsTrimmingStrategy;
import org.springframework.cloud.kubernetes.client.discovery.gson.ServiceTrimmingStrategy;
import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.context.annotation.Bean;
@@ -64,10 +62,6 @@ public class KubernetesDiscoveryClientAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public CatalogSharedInformerFactory catalogSharedInformerFactory(ApiClient apiClient) {
apiClient.getJSON()
.setGson(apiClient.getJSON().getGson().newBuilder()
.addDeserializationExclusionStrategy(new ServiceTrimmingStrategy())
.addDeserializationExclusionStrategy(new EndpointsTrimmingStrategy()).create());
return new CatalogSharedInformerFactory();
}

View File

@@ -40,6 +40,7 @@ import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesServiceInstance;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
public class KubernetesInformerDiscoveryClient implements DiscoveryClient, InitializingBean {
@@ -80,6 +81,10 @@ public class KubernetesInformerDiscoveryClient implements DiscoveryClient, Initi
public List<ServiceInstance> getInstances(String serviceId) {
Assert.notNull(serviceId, "[Assertion failed] - the object argument must not be null");
if (StringUtils.hasText(namespace) && !properties.isAllNamespaces()) {
log.warn("Namespace is null or empty, this may cause issues looking up services");
}
V1Service service = properties.isAllNamespaces() ? this.serviceLister.list().stream()
.filter(svc -> serviceId.equals(svc.getMetadata().getName())).findFirst().orElse(null)
: this.serviceLister.namespace(this.namespace).get(serviceId);

View File

@@ -121,7 +121,7 @@ public class KubernetesInformerDiscoveryClientTests {
assertThat(discoveryClient.getInstances("test-svc-1"))
.containsOnly(new KubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, new HashMap<>(), false));
verify(kubernetesDiscoveryProperties, times(1)).isAllNamespaces();
verify(kubernetesDiscoveryProperties, times(2)).isAllNamespaces();
}
private Lister<V1Service> setupServiceLister(V1Service... services) {

View File

@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-kubernetes</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>2.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-kubernetes-client-loadbalancer</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-client-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2013-2020 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;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.kubernetes.commons.ConditionalOnKubernetesEnabled;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesLoadBalancerProperties;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Ryan Baxter
*/
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(KubernetesLoadBalancerProperties.class)
@ConditionalOnKubernetesEnabled
@ConditionalOnProperty(value = "spring.cloud.kubernetes.loadbalancer.enabled", matchIfMissing = true)
@LoadBalancerClients(defaultConfiguration = KubernetesClientLoadBalancerClientConfiguration.class)
public class KubernetesClientLoadBalancerAutoConfiguration {
@Bean
KubernetesClientServiceInstanceMapper mapper(KubernetesLoadBalancerProperties properties,
KubernetesDiscoveryProperties discoveryProperties) {
return new KubernetesClientServiceInstanceMapper(properties, discoveryProperties);
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2013-2020 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;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServicesListSupplier;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
/**
* @author Ryan Baxter
*/
public class KubernetesClientLoadBalancerClientConfiguration {
@Bean
@ConditionalOnProperty(name = "spring.cloud.kubernetes.loadbalancer.mode", havingValue = "SERVICE")
KubernetesServicesListSupplier kubernetesServicesListSupplier(Environment environment, CoreV1Api coreV1Api,
KubernetesClientServiceInstanceMapper mapper, KubernetesDiscoveryProperties discoveryProperties,
KubernetesClientProperties kubernetesClientProperties) {
return new KubernetesClientServicesListSupplier(environment, mapper, discoveryProperties, coreV1Api,
kubernetesClientProperties);
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2013-2020 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;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.openapi.models.V1Service;
import io.kubernetes.client.openapi.models.V1ServicePort;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesServiceInstance;
import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesLoadBalancerProperties;
import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServiceInstanceMapper;
import org.springframework.util.StringUtils;
/**
* @author Ryan Baxter
*/
public class KubernetesClientServiceInstanceMapper implements KubernetesServiceInstanceMapper<V1Service> {
private KubernetesLoadBalancerProperties properties;
private KubernetesDiscoveryProperties discoveryProperties;
public KubernetesClientServiceInstanceMapper(KubernetesLoadBalancerProperties properties,
KubernetesDiscoveryProperties discoveryProperties) {
this.properties = properties;
this.discoveryProperties = discoveryProperties;
}
@Override
public KubernetesServiceInstance map(V1Service service) {
final V1ObjectMeta meta = service.getMetadata();
final List<V1ServicePort> ports = service.getSpec().getPorts();
V1ServicePort port = null;
if (ports.size() == 1) {
port = ports.get(0);
}
else if (ports.size() > 1 && StringUtils.hasText(this.properties.getPortName())) {
Optional<V1ServicePort> optPort = ports.stream()
.filter(it -> properties.getPortName().endsWith(it.getName())).findAny();
if (optPort.isPresent()) {
port = optPort.get();
}
}
if (port == null) {
return null;
}
final String host = KubernetesServiceInstanceMapper.createHost(service.getMetadata().getName(),
service.getMetadata().getNamespace(), properties.getClusterDomain());
final boolean secure = KubernetesServiceInstanceMapper.isSecure(service.getMetadata().getLabels(),
service.getMetadata().getAnnotations(), port.getName(), port.getPort());
return new KubernetesServiceInstance(meta.getUid(), meta.getName(), host, port.getPort(),
getServiceMetadata(service), secure);
}
private Map<String, String> getServiceMetadata(V1Service service) {
final Map<String, String> serviceMetadata = new HashMap<>();
KubernetesDiscoveryProperties.Metadata metadataProps = this.discoveryProperties.getMetadata();
if (metadataProps.isAddLabels()) {
Map<String, String> labelMetadata = KubernetesServiceInstanceMapper
.getMapWithPrefixedKeys(service.getMetadata().getLabels(), metadataProps.getLabelsPrefix());
serviceMetadata.putAll(labelMetadata);
}
if (metadataProps.isAddAnnotations()) {
Map<String, String> annotationMetadata = KubernetesServiceInstanceMapper.getMapWithPrefixedKeys(
service.getMetadata().getAnnotations(), metadataProps.getAnnotationsPrefix());
serviceMetadata.putAll(annotationMetadata);
}
return serviceMetadata;
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2013-2020 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;
import java.util.ArrayList;
import java.util.List;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.openapi.models.V1Service;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Flux;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServiceInstanceMapper;
import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServicesListSupplier;
import org.springframework.core.env.Environment;
/**
* @author Ryan Baxter
*/
public class KubernetesClientServicesListSupplier extends KubernetesServicesListSupplier {
private static final Log LOG = LogFactory.getLog(KubernetesClientServicesListSupplier.class);
private CoreV1Api coreV1Api;
private KubernetesClientProperties kubernetesClientProperties;
public KubernetesClientServicesListSupplier(Environment environment, KubernetesServiceInstanceMapper mapper,
KubernetesDiscoveryProperties discoveryProperties, CoreV1Api coreV1Api,
KubernetesClientProperties kubernetesClientProperties) {
super(environment, mapper, discoveryProperties);
this.coreV1Api = coreV1Api;
this.kubernetesClientProperties = kubernetesClientProperties;
}
@Override
public Flux<List<ServiceInstance>> get() {
LOG.info("Getting services with id " + this.getServiceId());
List<ServiceInstance> result = new ArrayList<>();
List<V1Service> services = null;
try {
if (discoveryProperties.isAllNamespaces()) {
services = coreV1Api.listServiceForAllNamespaces(null, null, "metadata.name=" + this.getServiceId(),
null, null, null, null, null, null).getItems();
}
else {
services = coreV1Api.listNamespacedService(kubernetesClientProperties.getNamespace(), null, null, null,
"metadata.name=" + this.getServiceId(), null, null, null, null, null).getItems();
}
services.forEach(service -> result.add(mapper.map(service)));
}
catch (ApiException e) {
LOG.warn("Error retrieving service with name " + this.getServiceId(), e);
}
LOG.info("Returning services: " + result);
return Flux.defer(() -> Flux.just(result));
}
}

View File

@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.kubernetes.client.loadbalancer.KubernetesClientLoadBalancerAutoConfiguration

View File

@@ -0,0 +1,124 @@
/*
* Copyright 2013-2020 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;
import java.io.IOException;
import java.util.Collections;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.util.ClientBuilder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.client.loadbalancer.LoadBalancerRequest;
import org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerDiscoveryClient;
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* @author Ryan Baxter
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = KubernetesClientLoadBalancerPodModeTests.App.class,
properties = { "spring.cloud.kubernetes.client.namespace=default" })
public class KubernetesClientLoadBalancerPodModeTests {
@Autowired
private TestRestTemplate rest;
@Test
public void testLoadBalancer() {
ResponseEntity<String> map = rest.getForEntity("/servicea", String.class);
assertThat(map.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@RestController
@SpringBootApplication
static class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
@Bean
public ApiClient apiClient() {
return new ClientBuilder().build();
}
@Bean
public BlockingLoadBalancerClient blockingLoadBalancerClient() {
BlockingLoadBalancerClient client = mock(BlockingLoadBalancerClient.class);
try {
ClientHttpResponse response = new MockClientHttpResponse("hello".getBytes(), HttpStatus.OK);
when(client.execute(eq("servicea-wiremock"), any(LoadBalancerRequest.class))).thenReturn(response);
when(client.execute(eq("servicea-wiremock"), any(ServiceInstance.class),
any(LoadBalancerRequest.class))).thenReturn(response);
}
catch (IOException e) {
e.printStackTrace();
}
return client;
}
@Bean
public KubernetesInformerDiscoveryClient kubernetesInformerDiscoveryClient() {
KubernetesInformerDiscoveryClient client = mock(KubernetesInformerDiscoveryClient.class);
ServiceInstance instance = new DefaultServiceInstance("servicea-wiremock1", "servicea-wiremock", "fake",
8888, false);
given(client.getInstances(eq("servicea-wiremock"))).willReturn(Collections.singletonList(instance));
return client;
}
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplateBuilder().build();
}
@GetMapping("/servicea")
public String greeting() {
return restTemplate().getForObject("http://servicea-wiremock", String.class);
}
}
}

View File

@@ -0,0 +1,157 @@
/*
* Copyright 2013-2020 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;
import java.io.IOException;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder;
import io.kubernetes.client.openapi.models.V1ServiceBuilder;
import io.kubernetes.client.openapi.models.V1ServiceList;
import io.kubernetes.client.openapi.models.V1ServiceListBuilder;
import io.kubernetes.client.openapi.models.V1ServicePortBuilder;
import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder;
import io.kubernetes.client.util.ClientBuilder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.client.loadbalancer.LoadBalancerRequest;
import org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerDiscoveryClient;
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* @author Ryan Baxter
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = KubernetesClientLoadBalancerServiceModeTests.App.class,
properties = { "spring.cloud.kubernetes.loadbalancer.mode=SERVICE",
"spring.cloud.kubernetes.client.namespace=default" })
public class KubernetesClientLoadBalancerServiceModeTests {
private static final V1ServiceList SERVICE_LIST = new V1ServiceListBuilder()
.addToItems(
new V1ServiceBuilder()
.withMetadata(new V1ObjectMetaBuilder().withName("servicea-wiremock")
.withNamespace("default").withResourceVersion("1").addToLabels("beta", "true")
.addToAnnotations("org.springframework.cloud", "true").withUid("0").build())
.withSpec(new V1ServiceSpecBuilder().withClusterIP("10.96.0.1").withSessionAffinity("None")
.withType("ClusterIP")
.addToPorts(new V1ServicePortBuilder().withPort(80).withName("http")
.withProtocol("TCP").withNewTargetPort(8080).build())
.build())
.build())
.build();
@Autowired
private TestRestTemplate rest;
@Test
public void testLoadBalancer() {
ResponseEntity<String> map = rest.getForEntity("/servicea", String.class);
assertThat(map.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@RestController
@SpringBootApplication
static class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
@Bean
public ApiClient apiClient() {
return new ClientBuilder().build();
}
@Bean
public CoreV1Api coreV1Api() {
CoreV1Api coreV1Api = mock(CoreV1Api.class);
try {
when(coreV1Api.listNamespacedService(eq("default"), eq(null), eq(null), eq(null),
eq("metadata.name=servicea-wiremock"), eq(null), eq(null), eq(null), eq(null), eq(null)))
.thenReturn(SERVICE_LIST);
}
catch (ApiException e) {
e.printStackTrace();
}
return coreV1Api;
}
@Bean
public BlockingLoadBalancerClient blockingLoadBalancerClient() {
BlockingLoadBalancerClient client = mock(BlockingLoadBalancerClient.class);
try {
ClientHttpResponse response = new MockClientHttpResponse("hello".getBytes(), HttpStatus.OK);
when(client.execute(eq("servicea-wiremock"), any(LoadBalancerRequest.class))).thenReturn(response);
when(client.execute(eq("servicea-wiremock"), any(ServiceInstance.class),
any(LoadBalancerRequest.class))).thenReturn(response);
}
catch (IOException e) {
e.printStackTrace();
}
return client;
}
@Bean
public KubernetesInformerDiscoveryClient kubernetesInformerDiscoveryClient() {
// Mock this so the real implementation does not try to connect to the K8S API
// Server
KubernetesInformerDiscoveryClient client = mock(KubernetesInformerDiscoveryClient.class);
return client;
}
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplateBuilder().build();
}
@GetMapping("/servicea")
public String greeting() {
return restTemplate().getForObject("http://servicea-wiremock", String.class);
}
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2013-2020 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;
import java.util.HashMap;
import java.util.Map;
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.V1ServicePortBuilder;
import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesServiceInstance;
import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesLoadBalancerProperties;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Ryan Baxter
*/
class KubernetesClientServiceInstanceMapperTests {
@Test
void basicMap() {
KubernetesLoadBalancerProperties loadBalancerProperties = new KubernetesLoadBalancerProperties();
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties();
KubernetesClientServiceInstanceMapper mapper = new KubernetesClientServiceInstanceMapper(loadBalancerProperties,
kubernetesDiscoveryProperties);
V1Service service = new V1ServiceBuilder()
.withMetadata(new V1ObjectMetaBuilder().withName("database").withUid("0").withResourceVersion("0")
.withNamespace("default").addToAnnotations("org.springframework.cloud", "true")
.addToLabels("beta", "true").build())
.withSpec(new V1ServiceSpecBuilder()
.addToPorts(new V1ServicePortBuilder().withPort(80).withName("http").build()).build())
.build();
KubernetesServiceInstance serviceInstance = mapper.map(service);
Map<String, String> metadata = new HashMap<>();
metadata.put("org.springframework.cloud", "true");
metadata.put("beta", "true");
KubernetesServiceInstance result = new KubernetesServiceInstance("0", "database",
"database.default.svc.cluster.local", 80, metadata, false);
assertThat(serviceInstance).isEqualTo(result);
}
@Test
void multiportMap() {
KubernetesLoadBalancerProperties loadBalancerProperties = new KubernetesLoadBalancerProperties();
loadBalancerProperties.setPortName("https");
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties();
KubernetesClientServiceInstanceMapper mapper = new KubernetesClientServiceInstanceMapper(loadBalancerProperties,
kubernetesDiscoveryProperties);
V1Service service = new V1ServiceBuilder()
.withMetadata(new V1ObjectMetaBuilder().withName("database").withUid("0").withResourceVersion("0")
.withNamespace("default").build())
.withSpec(new V1ServiceSpecBuilder()
.addToPorts(new V1ServicePortBuilder().withPort(80).withName("http").build(),
new V1ServicePortBuilder().withPort(443).withName("https").build())
.build())
.build();
KubernetesServiceInstance serviceInstance = mapper.map(service);
KubernetesServiceInstance result = new KubernetesServiceInstance("0", "database",
"database.default.svc.cluster.local", 443, new HashMap(), true);
assertThat(serviceInstance).isEqualTo(result);
}
}

View File

@@ -0,0 +1,173 @@
/*
* Copyright 2013-2020 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;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.Configuration;
import io.kubernetes.client.openapi.JSON;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder;
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 io.kubernetes.client.util.ClientBuilder;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesServiceInstance;
import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesLoadBalancerProperties;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.mock.env.MockEnvironment;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
/**
* @author Ryan Baxter
*/
class KubernetesClientServicesListSupplierTests {
private static final V1ServiceList SERVICE_LIST = new V1ServiceList().addItemsItem(new V1ServiceBuilder()
.withMetadata(new V1ObjectMetaBuilder().withName("service1").withNamespace("default")
.withResourceVersion("1").addToLabels("beta", "true")
.addToAnnotations("org.springframework.cloud", "true").withUid("0").build())
.withSpec(new V1ServiceSpecBuilder()
.addToPorts(new V1ServicePortBuilder().withPort(80).withName("http").build()).build())
.build());
private static final V1ServiceList SERVICE_LIST_ALL_NAMESPACE = new V1ServiceList()
.addItemsItem(new V1ServiceBuilder()
.withMetadata(new V1ObjectMetaBuilder().withName("service1").withNamespace("default")
.withResourceVersion("1").addToLabels("beta", "true")
.addToAnnotations("org.springframework.cloud", "true").withUid("0").build())
.withSpec(new V1ServiceSpecBuilder()
.addToPorts(new V1ServicePortBuilder().withPort(80).withName("http").build()).build())
.build())
.addItemsItem(new V1ServiceBuilder()
.withMetadata(new V1ObjectMetaBuilder().withName("service1").withNamespace("test")
.withResourceVersion("1").withUid("1").build())
.withSpec(new V1ServiceSpecBuilder()
.addToPorts(new V1ServicePortBuilder().withPort(80).withName("http").build(),
new V1ServicePortBuilder().withPort(443).withName("https").build())
.build())
.build());
private static WireMockServer wireMockServer;
@BeforeAll
public static void setup() {
wireMockServer = new WireMockServer(options().dynamicPort());
wireMockServer.start();
WireMock.configureFor("localhost", wireMockServer.port());
ApiClient client = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build();
client.setDebugging(true);
Configuration.setDefaultApiClient(client);
}
@AfterAll
public static void after() {
wireMockServer.stop();
}
@AfterEach
public void afterEach() {
WireMock.reset();
}
@Test
void getList() {
MockEnvironment env = new MockEnvironment();
env.setProperty(LoadBalancerClientFactory.PROPERTY_NAME, "service1");
KubernetesClientProperties kubernetesClientProperties = new KubernetesClientProperties();
kubernetesClientProperties.setNamespace("default");
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties();
CoreV1Api coreV1Api = new CoreV1Api();
KubernetesClientServiceInstanceMapper mapper = new KubernetesClientServiceInstanceMapper(
new KubernetesLoadBalancerProperties(), kubernetesDiscoveryProperties);
KubernetesClientServicesListSupplier listSupplier = new KubernetesClientServicesListSupplier(env, mapper,
kubernetesDiscoveryProperties, coreV1Api, kubernetesClientProperties);
stubFor(get(urlMatching("^/api/v1/namespaces/default/services.*"))
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(SERVICE_LIST))));
Flux<List<ServiceInstance>> instances = listSupplier.get();
Map<String, String> metadata = new HashMap<>();
metadata.put("org.springframework.cloud", "true");
metadata.put("beta", "true");
KubernetesServiceInstance service1 = new KubernetesServiceInstance("0", "service1",
"service1.default.svc.cluster.local", 80, metadata, false);
List<ServiceInstance> services = new ArrayList<>();
services.add(service1);
StepVerifier.create(instances).expectNext(services).verifyComplete();
}
@Test
void getListAllNamespaces() {
MockEnvironment env = new MockEnvironment();
env.setProperty(LoadBalancerClientFactory.PROPERTY_NAME, "service1");
KubernetesClientProperties kubernetesClientProperties = new KubernetesClientProperties();
kubernetesClientProperties.setNamespace("default");
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties();
kubernetesDiscoveryProperties.setAllNamespaces(true);
CoreV1Api coreV1Api = new CoreV1Api();
KubernetesClientServiceInstanceMapper mapper = new KubernetesClientServiceInstanceMapper(
new KubernetesLoadBalancerProperties(), kubernetesDiscoveryProperties);
KubernetesClientServicesListSupplier listSupplier = new KubernetesClientServicesListSupplier(env, mapper,
kubernetesDiscoveryProperties, coreV1Api, kubernetesClientProperties);
stubFor(get(urlMatching("^/api/v1/services.*"))
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(SERVICE_LIST_ALL_NAMESPACE))));
Flux<List<ServiceInstance>> instances = listSupplier.get();
Map<String, String> metadata = new HashMap<>();
metadata.put("org.springframework.cloud", "true");
metadata.put("beta", "true");
KubernetesServiceInstance service1 = new KubernetesServiceInstance("0", "service1",
"service1.default.svc.cluster.local", 80, metadata, false);
KubernetesServiceInstance service2 = new KubernetesServiceInstance("1", "service1",
"service1.test.svc.cluster.local", 80, new HashMap<>(), false);
List<ServiceInstance> services = new ArrayList<>();
services.add(service1);
services.add(service2);
StepVerifier.create(instances).expectNext(services).verifyComplete();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2019 the original author or authors.
* Copyright 2013-2020 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.
@@ -132,6 +132,13 @@ public class KubernetesServiceInstance implements ServiceInstance {
&& Objects.equals(metadata, that.metadata);
}
@Override
public String toString() {
return "KubernetesServiceInstance{" + "instanceId='" + instanceId + '\'' + ", serviceId='" + serviceId + '\''
+ ", host='" + host + '\'' + ", port=" + port + ", uri=" + uri + ", secure=" + secure + ", metadata="
+ metadata + '}';
}
@Override
public int hashCode() {
return Objects.hash(instanceId, serviceId, host, port, uri, secure, metadata);

View File

@@ -40,6 +40,7 @@
<istio-client.version>1.5.5</istio-client.version>
<mockwebserver.version>0.1.2</mockwebserver.version>
<okhttp.version>3.14.4</okhttp.version>
<wiremock.version>2.26.3</wiremock.version>
</properties>
<dependencyManagement>
<dependencies>
@@ -133,6 +134,13 @@
<artifactId>spring-cloud-kubernetes-fabric8-loadbalancer</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-client-loadbalancer</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-test-support</artifactId>
@@ -170,6 +178,12 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-client-loadbalancer</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-fabric8-all</artifactId>
@@ -259,6 +273,13 @@
</exclusions>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<version>${wiremock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>

View File

@@ -24,6 +24,7 @@ ALL_INTEGRATION_PROJECTS=(
"spring-cloud-kubernetes-core-k8s-client-it"
"spring-cloud-kubernetes-client-config-it"
"spring-cloud-kubernetes-configuration-watcher-it"
"spring-cloud-kubernetes-client-loadbalancer-it"
)
INTEGRATION_PROJECTS=(${INTEGRATION_PROJECTS:-${ALL_INTEGRATION_PROJECTS[@]}})

View File

@@ -0,0 +1,26 @@
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: spring-cloud-kubernetes-client-loadbalancer-it
name: spring-cloud-kubernetes-client-loadbalancer-it-deployment
spec:
replicas: 1
selector:
matchLabels:
app: spring-cloud-kubernetes-client-loadbalancer-it
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: spring-cloud-kubernetes-client-loadbalancer-it
spec:
serviceAccountName: spring-cloud-kubernetes-serviceaccount
containers:
- image: springcloud/spring-cloud-kubernetes-client-loadbalancer-it:2.0.0-SNAPSHOT
imagePullPolicy: IfNotPresent
name: spring-cloud-kubernetes-client-loadbalancer-it
resources: {}
status: {}

View File

@@ -0,0 +1,18 @@
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: spring-cloud-kubernetes-client-loadbalancer-it
name: spring-cloud-kubernetes-client-loadbalancer-it
spec:
ports:
- name: 80-8080
port: 80
protocol: TCP
targetPort: 8080
selector:
app: spring-cloud-kubernetes-client-loadbalancer-it
type: ClusterIP
status:
loadBalancer: {}

View File

@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-integration-tests</artifactId>
<version>2.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-kubernetes-client-loadbalancer-it</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-client-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-test-support</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.kubernetes</groupId>
<artifactId>client-java</artifactId>
</dependency>
<dependency>
<groupId>io.kubernetes</groupId>
<artifactId>client-java-extended</artifactId>
</dependency>
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java-transport-httpclient5</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<name>${env.IMAGE}</name>
</image>
<goal>build-image</goal>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>build-image</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>imagename</id>
<activation>
<property>
<name>!env.IMAGE</name>
</property>
</activation>
<properties>
<env.IMAGE>springcloud/${project.artifactId}:${project.version}</env.IMAGE>
</properties>
</profile>
</profiles>
</project>

View File

@@ -0,0 +1,19 @@
apiVersion: skaffold/v2alpha3
kind: Config
metadata:
name: spring-cloud-kubernetes-client-loadbalancer-it
build:
artifacts:
- image: springcloud/spring-cloud-kubernetes-client-loadbalancer-it
custom:
buildCommand: "../../mvnw clean install"
dependencies:
paths:
- src
- pom.xml
deploy:
kubectl:
manifests:
- k8s/deployment-it.yaml
- k8s/service-it.yaml
- ../permissions.yaml

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2013-2020 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 java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
/**
* @author Ryan Baxter
*/
@SpringBootApplication
@RestController
public class KubernetesClientLoadBalancerApplicationIt {
@Autowired
DiscoveryClient discoveryClient;
public static void main(String[] args) {
SpringApplication.run(KubernetesClientLoadBalancerApplicationIt.class, args);
}
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplateBuilder().build();
}
@GetMapping("/servicea")
public Map<String, Object> greeting() {
return restTemplate().getForObject("http://servicea-wiremock/__admin/mappings", Map.class);
}
@GetMapping("/services")
public List<String> services() {
return discoveryClient.getServices();
}
}

View File

@@ -0,0 +1,227 @@
/*
* Copyright 2013-2020 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 java.io.IOException;
import java.time.Duration;
import java.util.Map;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.apis.AppsV1Api;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.openapi.apis.NetworkingV1beta1Api;
import io.kubernetes.client.openapi.models.NetworkingV1beta1Ingress;
import io.kubernetes.client.openapi.models.V1Deployment;
import io.kubernetes.client.openapi.models.V1Service;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cloud.kubernetes.integration.tests.commons.K8SUtils;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.springframework.cloud.kubernetes.integration.tests.commons.K8SUtils.createApiClient;
/**
* @author Ryan Baxter
*/
public class LoadBalancerIT {
private static final Log LOG = LogFactory.getLog(LoadBalancerIT.class);
private static final String WIREMOCK_DEPLOYMENT_NAME = "servicea-wiremock-deployment";
private static final String WIREMOCK_APP_NAME = "servicea-wiremock";
private static final String SPRING_CLOUD_K8S_LOADBALANCER_DEPLOYMENT_NAME = "spring-cloud-kubernetes-client-loadbalancer-it-deployment";
private static final String SPRING_CLOUD_K8S_LOADBALANCER_APP_NAME = "spring-cloud-kubernetes-client-loadbalancer-it";
private static final String WIREMOCK_HOST = "localhost";
private static final String WIREMOCK_PATH = "/wiremock";
private static final int WIREMOCK_PORT = 80;
private static final String NAMESPACE = "default";
private ApiClient client;
private CoreV1Api api;
private AppsV1Api appsApi;
private NetworkingV1beta1Api networkingApi;
private K8SUtils k8SUtils;
@Before
public void setup() throws Exception {
this.client = createApiClient();
this.api = new CoreV1Api();
this.appsApi = new AppsV1Api();
this.networkingApi = new NetworkingV1beta1Api();
this.k8SUtils = new K8SUtils(api, appsApi);
deployWiremock();
// Check to make sure the wiremock deployment is ready
k8SUtils.waitForDeployment(WIREMOCK_DEPLOYMENT_NAME, NAMESPACE);
// Check to see if endpoint is ready
k8SUtils.waitForEndpointReady(WIREMOCK_APP_NAME, NAMESPACE);
}
@Test
public void testLoadBalancerServiceMode() throws Exception {
try {
deployLoadbalancerServiceIt();
testLoadBalancer();
}
finally {
cleanup();
}
}
@Test
public void testLoadBalancerPodMode() throws Exception {
try {
deployLoadbalancerPodIt();
testLoadBalancer();
}
finally {
cleanup();
}
}
private void cleanup() throws ApiException {
appsApi.deleteCollectionNamespacedDeployment(NAMESPACE, null, null, null,
"metadata.name=" + SPRING_CLOUD_K8S_LOADBALANCER_DEPLOYMENT_NAME, null, null, null, null, null, null,
null, null);
api.deleteNamespacedService(SPRING_CLOUD_K8S_LOADBALANCER_APP_NAME, NAMESPACE, null, null, null, null, null,
null);
networkingApi.deleteNamespacedIngress("it-ingress", NAMESPACE, null, null, null, null, null, null);
}
private void testLoadBalancer() throws Exception {
// Check to make sure the controller deployment is ready
k8SUtils.waitForDeployment(SPRING_CLOUD_K8S_LOADBALANCER_DEPLOYMENT_NAME, NAMESPACE);
RestTemplate rest = new RestTemplateBuilder().build();
rest.setErrorHandler(new ResponseErrorHandler() {
@Override
public boolean hasError(ClientHttpResponse clientHttpResponse) throws IOException {
LOG.warn("Received response status code: " + clientHttpResponse.getRawStatusCode());
if (clientHttpResponse.getRawStatusCode() == 503) {
return false;
}
return true;
}
@Override
public void handleError(ClientHttpResponse clientHttpResponse) throws IOException {
}
});
// Sometimes the NGINX ingress takes a bit to catch up and realize the service is
// available and we get a 503, we just need to wait a bit
await().timeout(Duration.ofSeconds(60))
.until(() -> rest.getForEntity("http://localhost:80/loadbalancer-it/servicea", String.class)
.getStatusCode().is2xxSuccessful());
Map<String, Object> result = rest.getForObject("http://localhost:80/loadbalancer-it/servicea", Map.class);
assertThat(result.containsKey("mappings")).isTrue();
assertThat(result.containsKey("meta")).isTrue();
}
@After
public void after() throws Exception {
appsApi.deleteCollectionNamespacedDeployment(NAMESPACE, null, null, null,
"metadata.name=" + WIREMOCK_DEPLOYMENT_NAME, null, null, null, null, null, null, null, null);
api.deleteNamespacedService(WIREMOCK_APP_NAME, NAMESPACE, null, null, null, null, null, null);
networkingApi.deleteNamespacedIngress("wiremock-ingress", NAMESPACE, null, null, null, null, null, null);
}
private void deployLoadbalancerServiceIt() throws Exception {
appsApi.createNamespacedDeployment(NAMESPACE, getLoadbalancerServiceItDeployment(), null, null, null);
api.createNamespacedService(NAMESPACE, getLoadbalancerItService(), null, null, null);
networkingApi.createNamespacedIngress(NAMESPACE, getLoadbalancerItIngress(), null, null, null);
}
private void deployLoadbalancerPodIt() throws Exception {
appsApi.createNamespacedDeployment(NAMESPACE, getLoadbalancerPodItDeployment(), null, null, null);
api.createNamespacedService(NAMESPACE, getLoadbalancerItService(), null, null, null);
networkingApi.createNamespacedIngress(NAMESPACE, getLoadbalancerItIngress(), null, null, null);
}
private V1Service getLoadbalancerItService() throws Exception {
V1Service service = (V1Service) k8SUtils
.readYamlFromClasspath("spring-cloud-kubernetes-client-loadbalancer-it-service.yaml");
return service;
}
private V1Deployment getLoadbalancerServiceItDeployment() throws Exception {
V1Deployment deployment = (V1Deployment) k8SUtils
.readYamlFromClasspath("spring-cloud-kubernetes-client-loadbalancer-service-it-deployment.yaml");
return deployment;
}
private V1Deployment getLoadbalancerPodItDeployment() throws Exception {
V1Deployment deployment = (V1Deployment) k8SUtils
.readYamlFromClasspath("spring-cloud-kubernetes-client-loadbalancer-service-it-deployment.yaml");
return deployment;
}
private NetworkingV1beta1Ingress getLoadbalancerItIngress() throws Exception {
NetworkingV1beta1Ingress ingress = (NetworkingV1beta1Ingress) k8SUtils
.readYamlFromClasspath("spring-cloud-kubernetes-client-loadbalancer-it-ingress.yaml");
return ingress;
}
private void deployWiremock() throws Exception {
appsApi.createNamespacedDeployment(NAMESPACE, getWireockDeployment(), null, null, null);
api.createNamespacedService(NAMESPACE, getWiremockAppService(), null, null, null);
networkingApi.createNamespacedIngress(NAMESPACE, getWiremockIngress(), null, null, null);
}
private NetworkingV1beta1Ingress getWiremockIngress() throws Exception {
NetworkingV1beta1Ingress ingress = (NetworkingV1beta1Ingress) k8SUtils
.readYamlFromClasspath("wiremock-ingress.yaml");
return ingress;
}
private V1Service getWiremockAppService() throws Exception {
V1Service service = (V1Service) k8SUtils.readYamlFromClasspath("wiremock-service.yaml");
return service;
}
private V1Deployment getWireockDeployment() throws Exception {
V1Deployment deployment = (V1Deployment) k8SUtils.readYamlFromClasspath("wiremock-deployment.yaml");
return deployment;
}
}

View File

@@ -0,0 +1,14 @@
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: it-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
rules:
- http:
paths:
- path: /loadbalancer-it(/|$)(.*)
backend:
serviceName: spring-cloud-kubernetes-client-loadbalancer-it
servicePort: 8080

View File

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

View File

@@ -0,0 +1,31 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-cloud-kubernetes-client-loadbalancer-it-deployment
spec:
selector:
matchLabels:
app: spring-cloud-kubernetes-client-loadbalancer-it
template:
metadata:
labels:
app: spring-cloud-kubernetes-client-loadbalancer-it
spec:
serviceAccountName: spring-cloud-kubernetes-serviceaccount
containers:
- name: spring-cloud-kubernetes-client-loadbalancer-it
env:
- name: SPRING_CLOUD_KUBERNETES_LOADBALANCER_MODE
value: POD
image: docker.io/springcloud/spring-cloud-kubernetes-client-loadbalancer-it:2.0.0-SNAPSHOT
imagePullPolicy: IfNotPresent
readinessProbe:
httpGet:
port: 8080
path: /actuator/health/readiness
livenessProbe:
httpGet:
port: 8080
path: /actuator/health/liveness
ports:
- containerPort: 8080

View File

@@ -0,0 +1,31 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-cloud-kubernetes-client-loadbalancer-it-deployment
spec:
selector:
matchLabels:
app: spring-cloud-kubernetes-client-loadbalancer-it
template:
metadata:
labels:
app: spring-cloud-kubernetes-client-loadbalancer-it
spec:
serviceAccountName: spring-cloud-kubernetes-serviceaccount
containers:
- name: spring-cloud-kubernetes-client-loadbalancer-it
env:
- name: SPRING_CLOUD_KUBERNETES_LOADBALANCER_MODE
value: SERVICE
image: docker.io/springcloud/spring-cloud-kubernetes-client-loadbalancer-it:2.0.0-SNAPSHOT
imagePullPolicy: IfNotPresent
readinessProbe:
httpGet:
port: 8080
path: /actuator/health/readiness
livenessProbe:
httpGet:
port: 8080
path: /actuator/health/liveness
ports:
- containerPort: 8080

View File

@@ -0,0 +1,27 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: servicea-wiremock-deployment
spec:
selector:
matchLabels:
app: servicea-wiremock
template:
metadata:
labels:
app: servicea-wiremock
spec:
containers:
- name: servicea-wiremock
image: rodolpheche/wiremock
imagePullPolicy: IfNotPresent
readinessProbe:
httpGet:
port: 8080
path: /__admin/mappings
livenessProbe:
httpGet:
port: 8080
path: /__admin/mappings
ports:
- containerPort: 8080

View File

@@ -0,0 +1,14 @@
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: wiremock-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
rules:
- http:
paths:
- path: /wiremock(/|$)(.*)
backend:
serviceName: servicea-wiremock
servicePort: 8080

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
labels:
app: servicea-wiremock
name: servicea-wiremock
spec:
ports:
- name: http
port: 8080
targetPort: 8080
selector:
app: servicea-wiremock
type: ClusterIP

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-kubernetes</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>2.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-starter-kubernetes-client-loadbalancer</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-client-autoconfig</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-client-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
</dependencies>
</project>