Merge pull request #610 from spring-cloud/lb-integration-hoxton
LB integration hoxton
This commit is contained in:
28
docs/src/main/asciidoc/load-balancer.adoc
Normal file
28
docs/src/main/asciidoc/load-balancer.adoc
Normal file
@@ -0,0 +1,28 @@
|
||||
== LoadBalancer for Kubernetes
|
||||
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.
|
||||
====
|
||||
[source,xml]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-kubernetes-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]
|
||||
----
|
||||
spring.cloud.kubernetes.loadbalancer.mode=SERVICE
|
||||
----
|
||||
====
|
||||
|
||||
To enabled load balancing across all namespaces use the following property. Property from `spring-cloud-kubernetes-discovery` module is respected.
|
||||
====
|
||||
[source]
|
||||
----
|
||||
spring.cloud.kubernetes.discovery.all-namespaces=true
|
||||
----
|
||||
====
|
||||
@@ -23,6 +23,8 @@ include::pod-health-indicator.adoc[]
|
||||
|
||||
include::leader-election.adoc[]
|
||||
|
||||
include::load-balancer.adoc[]
|
||||
|
||||
include::security-service-accounts.adoc[]
|
||||
|
||||
include::service-registry.adoc[]
|
||||
|
||||
2
pom.xml
2
pom.xml
@@ -99,6 +99,8 @@
|
||||
<module>spring-cloud-kubernetes-istio</module>
|
||||
<module>spring-cloud-kubernetes-integration-tests</module>
|
||||
<module>docs</module>
|
||||
<module>spring-cloud-kubernetes-loadbalancer</module>
|
||||
<module>spring-cloud-starter-kubernetes-loadbalancer</module>
|
||||
</modules>
|
||||
|
||||
<dependencyManagement>
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<properties>
|
||||
<arquillian.version>1.4.0.Final</arquillian.version>
|
||||
<arquillian-cube.version>1.15.2</arquillian-cube.version>
|
||||
<hoverfly.version>0.13.0</hoverfly.version>
|
||||
<kubernetes-client.version>4.10.3</kubernetes-client.version>
|
||||
<istio-client.version>1.0.0</istio-client.version>
|
||||
<mockwebserver.version>0.1.2</mockwebserver.version>
|
||||
@@ -98,6 +99,12 @@
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-kubernetes-loadbalancer</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Own dependencies - Starters -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
@@ -117,6 +124,12 @@
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-kubernetes-loadbalancer</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-kubernetes-all</artifactId>
|
||||
@@ -148,6 +161,18 @@
|
||||
<version>${arquillian-cube.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.specto</groupId>
|
||||
<artifactId>hoverfly-java-junit5</artifactId>
|
||||
<version>${hoverfly.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.specto</groupId>
|
||||
<artifactId>hoverfly-java</artifactId>
|
||||
<version>${hoverfly.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.fabric8</groupId>
|
||||
|
||||
@@ -162,7 +162,8 @@ public class KubernetesDiscoveryClient implements DiscoveryClient {
|
||||
|
||||
EndpointPort endpointPort = findEndpointPort(s);
|
||||
instances.add(new KubernetesServiceInstance(instanceId, serviceId,
|
||||
endpointAddress, endpointPort, endpointMetadata,
|
||||
endpointAddress.getIp(), endpointPort.getPort(),
|
||||
endpointMetadata,
|
||||
this.isServicePortSecureResolver
|
||||
.resolve(new DefaultIsServicePortSecureResolver.Input(
|
||||
endpointPort.getPort(),
|
||||
|
||||
@@ -19,9 +19,6 @@ package org.springframework.cloud.kubernetes.discovery;
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.EndpointAddress;
|
||||
import io.fabric8.kubernetes.api.model.EndpointPort;
|
||||
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
|
||||
/**
|
||||
@@ -37,43 +34,39 @@ public class KubernetesServiceInstance implements ServiceInstance {
|
||||
|
||||
private static final String DSL = "//";
|
||||
|
||||
private static final String COLN = ":";
|
||||
private static final String COLON = ":";
|
||||
|
||||
private final String instanceId;
|
||||
|
||||
private final String serviceId;
|
||||
|
||||
private final EndpointAddress endpointAddress;
|
||||
private final String host;
|
||||
|
||||
private final EndpointPort endpointPort;
|
||||
private final int port;
|
||||
|
||||
private final URI uri;
|
||||
|
||||
private final Boolean secure;
|
||||
|
||||
private final Map<String, String> metadata;
|
||||
|
||||
/**
|
||||
* @param instanceId the id of the instance.
|
||||
* @param serviceId the id of the service.
|
||||
* @param endpointAddress the address where the service instance can be found.
|
||||
* @param endpointPort the port on which the service is running.
|
||||
* @param host the address where the service instance can be found.
|
||||
* @param port the port on which the service is running.
|
||||
* @param metadata a map containing metadata.
|
||||
* @param secure indicates whether or not the connection needs to be secure.
|
||||
* @deprecated - use other constructor
|
||||
*/
|
||||
@Deprecated
|
||||
public KubernetesServiceInstance(String serviceId, EndpointAddress endpointAddress,
|
||||
EndpointPort endpointPort, Map<String, String> metadata, Boolean secure) {
|
||||
this(null, serviceId, endpointAddress, endpointPort, metadata, secure);
|
||||
}
|
||||
|
||||
public KubernetesServiceInstance(String instanceId, String serviceId,
|
||||
EndpointAddress endpointAddress, EndpointPort endpointPort,
|
||||
Map<String, String> metadata, Boolean secure) {
|
||||
public KubernetesServiceInstance(String instanceId, String serviceId, String host,
|
||||
int port, Map<String, String> metadata, Boolean secure) {
|
||||
this.instanceId = instanceId;
|
||||
this.serviceId = serviceId;
|
||||
this.endpointAddress = endpointAddress;
|
||||
this.endpointPort = endpointPort;
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.metadata = metadata;
|
||||
this.secure = secure;
|
||||
this.uri = createUri(secure ? HTTPS_PREFIX : HTTP_PREFIX, host, port);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -88,12 +81,12 @@ public class KubernetesServiceInstance implements ServiceInstance {
|
||||
|
||||
@Override
|
||||
public String getHost() {
|
||||
return this.endpointAddress.getIp();
|
||||
return this.host;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPort() {
|
||||
return this.endpointPort.getPort();
|
||||
return this.port;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -103,10 +96,7 @@ public class KubernetesServiceInstance implements ServiceInstance {
|
||||
|
||||
@Override
|
||||
public URI getUri() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(getScheme()).append(COLN).append(DSL).append(getHost()).append(COLN)
|
||||
.append(getPort());
|
||||
return URI.create(sb.toString());
|
||||
return uri;
|
||||
}
|
||||
|
||||
public Map<String, String> getMetadata() {
|
||||
@@ -118,4 +108,11 @@ public class KubernetesServiceInstance implements ServiceInstance {
|
||||
return isSecure() ? HTTPS_PREFIX : HTTP_PREFIX;
|
||||
}
|
||||
|
||||
private URI createUri(String scheme, String host, int port) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(scheme).append(COLON).append(DSL).append(host).append(COLON)
|
||||
.append(port);
|
||||
return URI.create(sb.toString());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -37,7 +37,8 @@ public class KubernetesServiceInstanceTests {
|
||||
EndpointPort port = new EndpointPort();
|
||||
port.setPort(8080);
|
||||
KubernetesServiceInstance instance = new KubernetesServiceInstance("123",
|
||||
"myservice", address, port, Collections.emptyMap(), secure);
|
||||
"myservice", address.getIp(), port.getPort(), Collections.emptyMap(),
|
||||
secure);
|
||||
|
||||
assertThat(instance.getInstanceId()).isEqualTo("123");
|
||||
assertThat(instance.getServiceId()).isEqualTo("myservice");
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<?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-integration-tests</artifactId>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<version>1.1.6.BUILD-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<name>Spring Cloud Kubernetes :: Integration Tests :: Load Balancer</name>
|
||||
<artifactId>load-balancer</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-kubernetes-loadbalancer</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-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.fabric8</groupId>
|
||||
<artifactId>kubernetes-server-mock</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.specto</groupId>
|
||||
<artifactId>hoverfly-java-junit5</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.specto</groupId>
|
||||
<artifactId>hoverfly-java</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.loadbalancer;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
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;
|
||||
|
||||
@SpringBootApplication
|
||||
@RestController
|
||||
public class SimpleLoadBalancerApp {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SimpleLoadBalancerApp.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@LoadBalanced
|
||||
RestTemplate restTemplate() {
|
||||
return new RestTemplateBuilder().build();
|
||||
}
|
||||
|
||||
@GetMapping("/greeting")
|
||||
public String greeting() {
|
||||
return "greeting";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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.loadbalancer;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.ServicePortBuilder;
|
||||
import io.fabric8.kubernetes.api.model.ServiceSpecBuilder;
|
||||
import io.fabric8.kubernetes.client.Config;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@TestPropertySource(properties = "spring.cloud.kubernetes.discovery.all-namespaces=true")
|
||||
@EnableKubernetesMockClient(crud = true)
|
||||
class LoadBalancerAllNamespacesTests {
|
||||
|
||||
@Autowired
|
||||
RestTemplate restTemplate;
|
||||
|
||||
@LocalServerPort
|
||||
int randomServerPort;
|
||||
|
||||
static KubernetesClient client;
|
||||
|
||||
@BeforeAll
|
||||
static void setup() {
|
||||
System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY,
|
||||
client.getConfiguration().getMasterUrl());
|
||||
System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true");
|
||||
System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false");
|
||||
System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY,
|
||||
"false");
|
||||
System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true");
|
||||
System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoadBalancerDifferentNamespace() {
|
||||
createTestData("service-b", "b");
|
||||
String response = restTemplate.getForObject("http://service-b/greeting",
|
||||
String.class);
|
||||
Assertions.assertNotNull(response);
|
||||
Assertions.assertEquals("greeting", response);
|
||||
}
|
||||
|
||||
private void createTestData(String name, String namespace) {
|
||||
client.services().inNamespace(namespace).createNew().withNewMetadata()
|
||||
.withName(name).withNamespace(namespace).endMetadata()
|
||||
.withSpec(new ServiceSpecBuilder().withPorts(new ServicePortBuilder()
|
||||
.withProtocol("TCP").withPort(randomServerPort).build()).build())
|
||||
.done();
|
||||
client.endpoints().inNamespace(namespace).createNew().withNewMetadata()
|
||||
.withName("service-a").withNamespace(namespace).endMetadata()
|
||||
.addNewSubset().addNewAddress().withIp("localhost").endAddress()
|
||||
.addNewPort().withName("http").withPort(randomServerPort).endPort()
|
||||
.endSubset().done();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.loadbalancer;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.ServicePortBuilder;
|
||||
import io.fabric8.kubernetes.api.model.ServiceSpecBuilder;
|
||||
import io.fabric8.kubernetes.client.Config;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@EnableKubernetesMockClient(crud = true)
|
||||
class LoadBalancerTests {
|
||||
|
||||
@Autowired
|
||||
RestTemplate restTemplate;
|
||||
|
||||
@LocalServerPort
|
||||
int randomServerPort;
|
||||
|
||||
static KubernetesClient client;
|
||||
|
||||
@BeforeAll
|
||||
static void setup() {
|
||||
System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY,
|
||||
client.getConfiguration().getMasterUrl());
|
||||
System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true");
|
||||
System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false");
|
||||
System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY,
|
||||
"false");
|
||||
System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true");
|
||||
System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoadBalancerSameNamespace() {
|
||||
createTestData("service-a", "test");
|
||||
String response = restTemplate.getForObject("http://service-a/greeting",
|
||||
String.class);
|
||||
Assertions.assertNotNull(response);
|
||||
Assertions.assertEquals("greeting", response);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoadBalancerDifferentNamespace() {
|
||||
createTestData("service-b", "b");
|
||||
Assertions.assertThrows(IllegalStateException.class, () -> restTemplate
|
||||
.getForObject("http://service-b/greeting", String.class));
|
||||
}
|
||||
|
||||
private void createTestData(String name, String namespace) {
|
||||
client.services().inNamespace(namespace).createNew().withNewMetadata()
|
||||
.withName(name).endMetadata()
|
||||
.withSpec(new ServiceSpecBuilder().withPorts(new ServicePortBuilder()
|
||||
.withProtocol("TCP").withPort(randomServerPort).build()).build())
|
||||
.done();
|
||||
client.endpoints().inNamespace(namespace).createNew().withNewMetadata()
|
||||
.withName("service-a").endMetadata().addNewSubset().addNewAddress()
|
||||
.withIp("localhost").endAddress().addNewPort().withName("http")
|
||||
.withPort(randomServerPort).endPort().endSubset().done();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.loadbalancer;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.Service;
|
||||
import io.fabric8.kubernetes.api.model.ServiceBuilder;
|
||||
import io.fabric8.kubernetes.client.Config;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.specto.hoverfly.junit.core.Hoverfly;
|
||||
import io.specto.hoverfly.junit5.HoverflyExtension;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import static io.specto.hoverfly.junit.core.SimulationSource.dsl;
|
||||
import static io.specto.hoverfly.junit.dsl.HoverflyDsl.service;
|
||||
import static io.specto.hoverfly.junit.dsl.HttpBodyConverter.json;
|
||||
import static io.specto.hoverfly.junit.dsl.ResponseCreators.success;
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@TestPropertySource(properties = { "spring.cloud.kubernetes.loadbalancer.mode=SERVICE",
|
||||
"spring.cloud.kubernetes.loadbalancer.enabled=true" })
|
||||
@ExtendWith(HoverflyExtension.class)
|
||||
class LoadBalancerWithServiceTests {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory
|
||||
.getLogger(LoadBalancerWithServiceTests.class);
|
||||
|
||||
@Autowired
|
||||
RestTemplate restTemplate;
|
||||
|
||||
@Autowired
|
||||
KubernetesClient client;
|
||||
|
||||
@BeforeAll
|
||||
static void setup() {
|
||||
System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true");
|
||||
System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false");
|
||||
System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY,
|
||||
"false");
|
||||
System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true");
|
||||
System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoadBalancerInServiceMode(Hoverfly hoverfly) {
|
||||
LOGGER.info("Master URL: {}", client.getConfiguration().getMasterUrl());
|
||||
hoverfly.simulate(
|
||||
dsl(service("http://service-a.test.svc.cluster.local:8080")
|
||||
.get("/greeting").willReturn(success().body("greeting"))),
|
||||
dsl(service(client.getConfiguration().getMasterUrl().replace("/", "")
|
||||
.replace("https:", ""))
|
||||
.get("/api/v1/namespaces/test/services/service-a")
|
||||
.willReturn(success().body(
|
||||
json(buildService("service-a", 8080, "test"))))));
|
||||
String response = restTemplate.getForObject("http://service-a/greeting",
|
||||
String.class);
|
||||
Assertions.assertNotNull(response);
|
||||
Assertions.assertEquals("greeting", response);
|
||||
}
|
||||
|
||||
private Service buildService(String name, int port, String namespace) {
|
||||
return new ServiceBuilder().withNewMetadata().withName(name)
|
||||
.withNamespace(namespace).withLabels(new HashMap<>())
|
||||
.withAnnotations(new HashMap<>()).endMetadata().withNewSpec().addNewPort()
|
||||
.withPort(port).endPort().endSpec().build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -144,7 +144,6 @@
|
||||
<module>simple-configmap</module>
|
||||
<!-- <module>istio</module>-->
|
||||
<module>discovery</module>
|
||||
</modules>
|
||||
|
||||
|
||||
<module>load-balancer</module>
|
||||
</modules>
|
||||
</project>
|
||||
|
||||
50
spring-cloud-kubernetes-loadbalancer/pom.xml
Normal file
50
spring-cloud-kubernetes-loadbalancer/pom.xml
Normal file
@@ -0,0 +1,50 @@
|
||||
<?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</artifactId>
|
||||
<version>1.1.6.BUILD-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>spring-cloud-kubernetes-loadbalancer</artifactId>
|
||||
<name>Spring Cloud Kubernetes :: Load Balancer</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-kubernetes-discovery</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-lang</groupId>
|
||||
<artifactId>commons-lang</artifactId>
|
||||
<version>2.6</version>
|
||||
</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>io.fabric8</groupId>
|
||||
<artifactId>kubernetes-server-mock</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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.loadbalancer;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryProperties;
|
||||
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Kubernetes load balancer auto-configuration.
|
||||
*
|
||||
* @author Piotr Minkowski
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableConfigurationProperties(KubernetesLoadBalancerProperties.class)
|
||||
@ConditionalOnProperty(value = "spring.cloud.kubernetes.loadbalancer.enabled",
|
||||
matchIfMissing = true)
|
||||
@LoadBalancerClients(
|
||||
defaultConfiguration = KubernetesLoadBalancerClientConfiguration.class)
|
||||
public class KubernetesLoadBalancerAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
KubernetesServiceInstanceMapper mapper(KubernetesLoadBalancerProperties properties,
|
||||
KubernetesDiscoveryProperties discoveryProperties) {
|
||||
return new KubernetesServiceInstanceMapper(properties, discoveryProperties);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.loadbalancer;
|
||||
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
/**
|
||||
* Kubernetes load balancer client configuration.
|
||||
*
|
||||
* @author Piotr Minkowski
|
||||
*/
|
||||
public class KubernetesLoadBalancerClientConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "spring.cloud.kubernetes.loadbalancer.mode",
|
||||
havingValue = "SERVICE")
|
||||
KubernetesServicesListSupplier kubernetesServicesListSupplier(Environment environment,
|
||||
KubernetesClient kubernetesClient, KubernetesServiceInstanceMapper mapper,
|
||||
KubernetesDiscoveryProperties discoveryProperties) {
|
||||
return new KubernetesServicesListSupplier(environment, kubernetesClient, mapper,
|
||||
discoveryProperties);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2013-2019 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.loadbalancer;
|
||||
|
||||
/**
|
||||
* Kubernetes load balancer mode enum.
|
||||
*
|
||||
* @author Piotr Minkowski
|
||||
*/
|
||||
public enum KubernetesLoadBalancerMode {
|
||||
|
||||
/**
|
||||
* using pod ip and port.
|
||||
*/
|
||||
POD,
|
||||
/**
|
||||
* using kubernetes service name and port.
|
||||
*/
|
||||
SERVICE
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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.loadbalancer;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* Kubernetes load balancer client properties.
|
||||
*
|
||||
* @author Piotr Minkowski
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "spring.cloud.kubernetes.loadbalancer")
|
||||
public class KubernetesLoadBalancerProperties {
|
||||
|
||||
/**
|
||||
* Load balancer enabled,default true.
|
||||
*/
|
||||
private Boolean enabled = true;
|
||||
|
||||
/**
|
||||
* {@link KubernetesLoadBalancerMode} setting load balancer server list with ip of pod
|
||||
* or service name. default value is POD.
|
||||
*/
|
||||
private KubernetesLoadBalancerMode mode = KubernetesLoadBalancerMode.POD;
|
||||
|
||||
/**
|
||||
* cluster domain.
|
||||
*/
|
||||
private String clusterDomain = "cluster.local";
|
||||
|
||||
/**
|
||||
* service port name.
|
||||
*/
|
||||
private String portName = "http";
|
||||
|
||||
/**
|
||||
* Get cluster domain.
|
||||
* @return the cluster domain
|
||||
*/
|
||||
public String getClusterDomain() {
|
||||
return clusterDomain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets cluster domain.
|
||||
* @param clusterDomain the cluster domain
|
||||
*/
|
||||
public void setClusterDomain(String clusterDomain) {
|
||||
this.clusterDomain = clusterDomain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets mode.
|
||||
* @return the mode
|
||||
*/
|
||||
public KubernetesLoadBalancerMode getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets mode.
|
||||
* @param mode the mode
|
||||
*/
|
||||
public void setMode(KubernetesLoadBalancerMode mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets enabled.
|
||||
* @return the enabled
|
||||
*/
|
||||
public Boolean getEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets enabled.
|
||||
* @param enabled the enabled
|
||||
*/
|
||||
public void setEnabled(Boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets portName.
|
||||
* @return portName port name
|
||||
*/
|
||||
public String getPortName() {
|
||||
return portName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets portName.
|
||||
* @param portName port name
|
||||
*/
|
||||
public void setPortName(String portName) {
|
||||
this.portName = portName;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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.loadbalancer;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.ObjectMeta;
|
||||
import io.fabric8.kubernetes.api.model.Service;
|
||||
import io.fabric8.kubernetes.api.model.ServicePort;
|
||||
import io.fabric8.kubernetes.client.utils.Utils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
import org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryProperties;
|
||||
import org.springframework.cloud.kubernetes.discovery.KubernetesServiceInstance;
|
||||
|
||||
/**
|
||||
* Class for mapping Kubernetes Service object into {@link KubernetesServiceInstance}.
|
||||
*
|
||||
* @author Piotr Minkowski
|
||||
*/
|
||||
public class KubernetesServiceInstanceMapper {
|
||||
|
||||
private final KubernetesLoadBalancerProperties properties;
|
||||
|
||||
private final KubernetesDiscoveryProperties discoveryProperties;
|
||||
|
||||
KubernetesServiceInstanceMapper(KubernetesLoadBalancerProperties properties,
|
||||
KubernetesDiscoveryProperties discoveryProperties) {
|
||||
this.properties = properties;
|
||||
this.discoveryProperties = discoveryProperties;
|
||||
}
|
||||
|
||||
public KubernetesServiceInstance map(Service service) {
|
||||
final ObjectMeta meta = service.getMetadata();
|
||||
final List<ServicePort> ports = service.getSpec().getPorts();
|
||||
ServicePort port = null;
|
||||
if (ports.size() == 1) {
|
||||
port = ports.get(0);
|
||||
}
|
||||
else if (ports.size() > 1
|
||||
&& Utils.isNotNullOrEmpty(this.properties.getPortName())) {
|
||||
Optional<ServicePort> 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 = createHost(service);
|
||||
final boolean secure = isSecure(service, port);
|
||||
return new KubernetesServiceInstance(meta.getUid(), meta.getName(), host,
|
||||
port.getPort(), getServiceMetadata(service), secure);
|
||||
}
|
||||
|
||||
private Map<String, String> getServiceMetadata(Service service) {
|
||||
final Map<String, String> serviceMetadata = new HashMap<>();
|
||||
KubernetesDiscoveryProperties.Metadata metadataProps = this.discoveryProperties
|
||||
.getMetadata();
|
||||
if (metadataProps.isAddLabels()) {
|
||||
Map<String, String> labelMetadata = getMapWithPrefixedKeys(
|
||||
service.getMetadata().getLabels(), metadataProps.getLabelsPrefix());
|
||||
serviceMetadata.putAll(labelMetadata);
|
||||
}
|
||||
if (metadataProps.isAddAnnotations()) {
|
||||
Map<String, String> annotationMetadata = getMapWithPrefixedKeys(
|
||||
service.getMetadata().getAnnotations(),
|
||||
metadataProps.getAnnotationsPrefix());
|
||||
serviceMetadata.putAll(annotationMetadata);
|
||||
}
|
||||
|
||||
return serviceMetadata;
|
||||
}
|
||||
|
||||
private Map<String, String> getMapWithPrefixedKeys(Map<String, String> map,
|
||||
String prefix) {
|
||||
if (map == null) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
if (!org.springframework.util.StringUtils.hasText(prefix)) {
|
||||
return map;
|
||||
}
|
||||
final Map<String, String> result = new HashMap<>();
|
||||
map.forEach((k, v) -> result.put(prefix + k, v));
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean isSecure(Service service, ServicePort port) {
|
||||
final String securedLabelValue = service.getMetadata().getLabels()
|
||||
.getOrDefault("secured", "false");
|
||||
if (securedLabelValue.equals("true")) {
|
||||
return true;
|
||||
}
|
||||
final String securedAnnotationValue = service.getMetadata().getAnnotations()
|
||||
.getOrDefault("secured", "false");
|
||||
if (securedAnnotationValue.equals("true")) {
|
||||
return true;
|
||||
}
|
||||
return (port.getName() != null && port.getName().endsWith("https"))
|
||||
|| port.getPort().toString().endsWith("443");
|
||||
}
|
||||
|
||||
private String createHost(Service service) {
|
||||
return String.format("%s.%s.svc.%s", service.getMetadata().getName(),
|
||||
StringUtils.isNotBlank(service.getMetadata().getNamespace())
|
||||
? service.getMetadata().getNamespace() : "default",
|
||||
properties.getClusterDomain());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.loadbalancer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.Service;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryProperties;
|
||||
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
/**
|
||||
* Implementation of {@link ServiceInstanceListSupplier} for load balancer in SERVICE
|
||||
* mode.
|
||||
*
|
||||
* @author Piotr Minkowski
|
||||
*/
|
||||
public class KubernetesServicesListSupplier implements ServiceInstanceListSupplier {
|
||||
|
||||
private final Environment environment;
|
||||
|
||||
private final KubernetesClient kubernetesClient;
|
||||
|
||||
private final KubernetesDiscoveryProperties discoveryProperties;
|
||||
|
||||
private final KubernetesServiceInstanceMapper mapper;
|
||||
|
||||
KubernetesServicesListSupplier(Environment environment,
|
||||
KubernetesClient kubernetesClient, KubernetesServiceInstanceMapper mapper,
|
||||
KubernetesDiscoveryProperties discoveryProperties) {
|
||||
this.environment = environment;
|
||||
this.kubernetesClient = kubernetesClient;
|
||||
this.discoveryProperties = discoveryProperties;
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServiceId() {
|
||||
return environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<List<ServiceInstance>> get() {
|
||||
List<ServiceInstance> result = new ArrayList<>();
|
||||
if (discoveryProperties.isAllNamespaces()) {
|
||||
List<Service> services = this.kubernetesClient.services().inAnyNamespace()
|
||||
.withField("metadata.name", this.getServiceId()).list().getItems();
|
||||
services.forEach(service -> result.add(mapper.map(service)));
|
||||
}
|
||||
else {
|
||||
Service service = StringUtils.isNotBlank(this.kubernetesClient.getNamespace())
|
||||
? this.kubernetesClient.services()
|
||||
.inNamespace(this.kubernetesClient.getNamespace())
|
||||
.withName(this.getServiceId()).get()
|
||||
: this.kubernetesClient.services().withName(this.getServiceId())
|
||||
.get();
|
||||
if (service != null) {
|
||||
result.add(mapper.map(service));
|
||||
}
|
||||
}
|
||||
return Flux.defer(() -> Flux.just(result));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.springframework.cloud.kubernetes.loadbalancer.KubernetesLoadBalancerAutoConfiguration
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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.loadbalancer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.Service;
|
||||
import io.fabric8.kubernetes.api.model.ServiceBuilder;
|
||||
import io.fabric8.kubernetes.api.model.ServicePort;
|
||||
import io.fabric8.kubernetes.api.model.ServicePortBuilder;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryProperties;
|
||||
import org.springframework.cloud.kubernetes.discovery.KubernetesServiceInstance;
|
||||
|
||||
class KubernetesServiceInstanceMapperTests {
|
||||
|
||||
@Test
|
||||
public void testMapperSimple() {
|
||||
KubernetesLoadBalancerProperties properties = new KubernetesLoadBalancerProperties();
|
||||
KubernetesDiscoveryProperties discoveryProperties = new KubernetesDiscoveryProperties();
|
||||
Service service = buildService("test", "abc", 8080, null, new HashMap<>());
|
||||
KubernetesServiceInstance instance = new KubernetesServiceInstanceMapper(
|
||||
properties, discoveryProperties).map(service);
|
||||
Assertions.assertNotNull(instance);
|
||||
Assertions.assertEquals("test", instance.getServiceId());
|
||||
Assertions.assertEquals("abc", instance.getInstanceId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMapperMultiplePorts() {
|
||||
KubernetesLoadBalancerProperties properties = new KubernetesLoadBalancerProperties();
|
||||
properties.setPortName("http");
|
||||
KubernetesDiscoveryProperties discoveryProperties = new KubernetesDiscoveryProperties();
|
||||
List<ServicePort> ports = new ArrayList<>();
|
||||
ports.add(new ServicePortBuilder().withPort(8080).withName("web").build());
|
||||
ports.add(new ServicePortBuilder().withPort(9000).withName("http").build());
|
||||
Service service = buildService("test", "abc", ports, new HashMap<>());
|
||||
KubernetesServiceInstance instance = new KubernetesServiceInstanceMapper(
|
||||
properties, discoveryProperties).map(service);
|
||||
Assertions.assertNotNull(instance);
|
||||
Assertions.assertEquals("test", instance.getServiceId());
|
||||
Assertions.assertEquals("abc", instance.getInstanceId());
|
||||
Assertions.assertEquals(9000, instance.getPort());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMapperSecure() {
|
||||
KubernetesLoadBalancerProperties properties = new KubernetesLoadBalancerProperties();
|
||||
KubernetesDiscoveryProperties discoveryProperties = new KubernetesDiscoveryProperties();
|
||||
Service service = buildService("test", "abc", 443, null, new HashMap<>());
|
||||
KubernetesServiceInstance instance = new KubernetesServiceInstanceMapper(
|
||||
properties, discoveryProperties).map(service);
|
||||
Assertions.assertNotNull(instance);
|
||||
Assertions.assertEquals("test", instance.getServiceId());
|
||||
Assertions.assertEquals("abc", instance.getInstanceId());
|
||||
Assertions.assertTrue(instance.isSecure());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMapperSecureWithLabels() {
|
||||
KubernetesLoadBalancerProperties properties = new KubernetesLoadBalancerProperties();
|
||||
KubernetesDiscoveryProperties discoveryProperties = new KubernetesDiscoveryProperties();
|
||||
HashMap<String, String> labels = new HashMap<>();
|
||||
labels.put("secured", "true");
|
||||
labels.put("label1", "123");
|
||||
Service service = buildService("test", "abc", 8080, null, labels);
|
||||
KubernetesServiceInstance instance = new KubernetesServiceInstanceMapper(
|
||||
properties, discoveryProperties).map(service);
|
||||
Assertions.assertNotNull(instance);
|
||||
Assertions.assertEquals("test", instance.getServiceId());
|
||||
Assertions.assertEquals("abc", instance.getInstanceId());
|
||||
Assertions.assertTrue(instance.isSecure());
|
||||
Assertions.assertEquals(2, instance.getMetadata().keySet().size());
|
||||
}
|
||||
|
||||
private Service buildService(String name, String uid, List<ServicePort> ports,
|
||||
Map<String, String> labels) {
|
||||
return new ServiceBuilder().withNewMetadata().withName(name).withNewUid(uid)
|
||||
.addToLabels(labels).addToAnnotations(new HashMap<>(0)).endMetadata()
|
||||
.withNewSpec().addAllToPorts(ports).endSpec().build();
|
||||
}
|
||||
|
||||
private Service buildService(String name, String uid, int port, String portName,
|
||||
Map<String, String> labels) {
|
||||
ServicePort servicePort = new ServicePortBuilder().withPort(port)
|
||||
.withName(portName).build();
|
||||
return buildService(name, uid, Collections.singletonList(servicePort), labels);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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.loadbalancer;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.DoneableService;
|
||||
import io.fabric8.kubernetes.api.model.Service;
|
||||
import io.fabric8.kubernetes.api.model.ServiceBuilder;
|
||||
import io.fabric8.kubernetes.api.model.ServiceList;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.fabric8.kubernetes.client.Watch;
|
||||
import io.fabric8.kubernetes.client.Watcher;
|
||||
import io.fabric8.kubernetes.client.dsl.FilterWatchListMultiDeletable;
|
||||
import io.fabric8.kubernetes.client.dsl.MixedOperation;
|
||||
import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation;
|
||||
import io.fabric8.kubernetes.client.dsl.ServiceResource;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryProperties;
|
||||
import org.springframework.cloud.kubernetes.discovery.KubernetesServiceInstance;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class KubernetesServiceListSupplierTests {
|
||||
|
||||
@Mock
|
||||
Environment environment;
|
||||
|
||||
@Mock
|
||||
KubernetesServiceInstanceMapper mapper;
|
||||
|
||||
@Mock
|
||||
KubernetesClient client;
|
||||
|
||||
@Mock
|
||||
MixedOperation<Service, ServiceList, DoneableService, ServiceResource<Service, DoneableService>> serviceOperation;
|
||||
|
||||
@Mock
|
||||
NonNamespaceOperation<Service, ServiceList, DoneableService, ServiceResource<Service, DoneableService>> namespaceOperation;
|
||||
|
||||
@Mock
|
||||
ServiceResource<Service, DoneableService> serviceResource;
|
||||
|
||||
@Mock
|
||||
FilterWatchListMultiDeletable<Service, ServiceList, Boolean, Watch, Watcher<Service>> multiDeletable;
|
||||
|
||||
@Test
|
||||
void testPositiveMatch() {
|
||||
when(environment.getProperty("loadbalancer.client.name"))
|
||||
.thenReturn("test-service");
|
||||
when(mapper.map(any(Service.class)))
|
||||
.thenReturn(new KubernetesServiceInstance("", "", "", 0, null, false));
|
||||
when(this.client.getNamespace()).thenReturn("test");
|
||||
when(this.client.services()).thenReturn(this.serviceOperation);
|
||||
when(this.serviceOperation.inNamespace("test")).thenReturn(namespaceOperation);
|
||||
when(this.namespaceOperation.withName("test-service"))
|
||||
.thenReturn(this.serviceResource);
|
||||
when(this.serviceResource.get()).thenReturn(buildService("test-service", 8080));
|
||||
KubernetesServicesListSupplier supplier = new KubernetesServicesListSupplier(
|
||||
environment, client, mapper, new KubernetesDiscoveryProperties());
|
||||
List<ServiceInstance> instances = supplier.get().blockFirst();
|
||||
assert instances != null;
|
||||
Assertions.assertEquals(1, instances.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPositiveMatchAllNamespaces() {
|
||||
when(environment.getProperty("loadbalancer.client.name"))
|
||||
.thenReturn("test-service");
|
||||
when(mapper.map(any(Service.class)))
|
||||
.thenReturn(new KubernetesServiceInstance("", "", "", 0, null, false));
|
||||
when(this.client.services()).thenReturn(this.serviceOperation);
|
||||
when(this.serviceOperation.inAnyNamespace()).thenReturn(this.multiDeletable);
|
||||
when(this.multiDeletable.withField("metadata.name", "test-service"))
|
||||
.thenReturn(this.multiDeletable);
|
||||
ServiceList serviceList = new ServiceList();
|
||||
serviceList.getItems().add(buildService("test-service", 8080));
|
||||
when(this.multiDeletable.list()).thenReturn(serviceList);
|
||||
KubernetesDiscoveryProperties discoveryProperties = new KubernetesDiscoveryProperties();
|
||||
discoveryProperties.setAllNamespaces(true);
|
||||
KubernetesServicesListSupplier supplier = new KubernetesServicesListSupplier(
|
||||
environment, client, mapper, discoveryProperties);
|
||||
List<ServiceInstance> instances = supplier.get().blockFirst();
|
||||
assert instances != null;
|
||||
Assertions.assertEquals(1, instances.size());
|
||||
}
|
||||
|
||||
private Service buildService(String name, int port) {
|
||||
return new ServiceBuilder().withNewMetadata().withName(name).endMetadata()
|
||||
.withNewSpec().addNewPort().withPort(port).endPort().endSpec().build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -51,6 +51,11 @@
|
||||
<artifactId>spring-cloud-kubernetes-ribbon</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-kubernetes-loadbalancer</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
30
spring-cloud-starter-kubernetes-loadbalancer/pom.xml
Normal file
30
spring-cloud-starter-kubernetes-loadbalancer/pom.xml
Normal 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>1.1.6.BUILD-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>spring-cloud-starter-kubernetes-loadbalancer</artifactId>
|
||||
<name>Spring Cloud Kubernetes :: Starter :: LoadBalancer</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-kubernetes-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-kubernetes-loadbalancer</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
Reference in New Issue
Block a user