Merge pull request #610 from spring-cloud/lb-integration-hoxton

LB integration hoxton
This commit is contained in:
Olga Maciaszek-Sharma
2020-08-28 05:21:09 -05:00
committed by GitHub
25 changed files with 1202 additions and 31 deletions

View 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
----
====

View File

@@ -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[]

View File

@@ -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>

View File

@@ -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>

View File

@@ -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(),

View File

@@ -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());
}
}

View File

@@ -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");

View File

@@ -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>

View File

@@ -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";
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

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.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();
}
}

View File

@@ -144,7 +144,6 @@
<module>simple-configmap</module>
<!-- <module>istio</module>-->
<module>discovery</module>
</modules>
<module>load-balancer</module>
</modules>
</project>

View 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>

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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));
}
}

View File

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

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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>

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>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>