diff --git a/docs/src/main/asciidoc/load-balancer.adoc b/docs/src/main/asciidoc/load-balancer.adoc
new file mode 100644
index 00000000..bdb4813c
--- /dev/null
+++ b/docs/src/main/asciidoc/load-balancer.adoc
@@ -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]
+----
+
+ org.springframework.cloud
+ spring-cloud-starter-kubernetes-loadbalancer
+
+----
+====
+
+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
+----
+====
diff --git a/docs/src/main/asciidoc/spring-cloud-kubernetes.adoc b/docs/src/main/asciidoc/spring-cloud-kubernetes.adoc
index 913d803e..4876f418 100644
--- a/docs/src/main/asciidoc/spring-cloud-kubernetes.adoc
+++ b/docs/src/main/asciidoc/spring-cloud-kubernetes.adoc
@@ -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[]
diff --git a/pom.xml b/pom.xml
index 2a7f6a51..3dee885e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -99,6 +99,8 @@
spring-cloud-kubernetes-istio
spring-cloud-kubernetes-integration-tests
docs
+ spring-cloud-kubernetes-loadbalancer
+ spring-cloud-starter-kubernetes-loadbalancer
diff --git a/spring-cloud-kubernetes-dependencies/pom.xml b/spring-cloud-kubernetes-dependencies/pom.xml
index 990d76f3..165c41e4 100644
--- a/spring-cloud-kubernetes-dependencies/pom.xml
+++ b/spring-cloud-kubernetes-dependencies/pom.xml
@@ -34,6 +34,7 @@
1.4.0.Final
1.15.2
+ 0.13.0
4.10.3
1.0.0
0.1.2
@@ -98,6 +99,12 @@
${project.version}
+
+ org.springframework.cloud
+ spring-cloud-kubernetes-loadbalancer
+ ${project.version}
+
+
org.springframework.cloud
@@ -117,6 +124,12 @@
${project.version}
+
+ org.springframework.cloud
+ spring-cloud-starter-kubernetes-loadbalancer
+ ${project.version}
+
+
org.springframework.cloud
spring-cloud-starter-kubernetes-all
@@ -148,6 +161,18 @@
${arquillian-cube.version}
test
+
+ io.specto
+ hoverfly-java-junit5
+ ${hoverfly.version}
+ test
+
+
+ io.specto
+ hoverfly-java
+ ${hoverfly.version}
+ test
+
io.fabric8
diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClient.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClient.java
index ac8eb75d..c1747f55 100644
--- a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClient.java
+++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClient.java
@@ -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(),
diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesServiceInstance.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesServiceInstance.java
index 9c1a6180..d11ad1c9 100644
--- a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesServiceInstance.java
+++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesServiceInstance.java
@@ -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 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 metadata, Boolean secure) {
- this(null, serviceId, endpointAddress, endpointPort, metadata, secure);
- }
-
- public KubernetesServiceInstance(String instanceId, String serviceId,
- EndpointAddress endpointAddress, EndpointPort endpointPort,
- Map metadata, Boolean secure) {
+ public KubernetesServiceInstance(String instanceId, String serviceId, String host,
+ int port, Map 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 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());
+ }
+
}
diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesServiceInstanceTests.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesServiceInstanceTests.java
index eb3738e5..8cf648c6 100644
--- a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesServiceInstanceTests.java
+++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesServiceInstanceTests.java
@@ -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");
diff --git a/spring-cloud-kubernetes-integration-tests/load-balancer/pom.xml b/spring-cloud-kubernetes-integration-tests/load-balancer/pom.xml
new file mode 100644
index 00000000..e6d25022
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/load-balancer/pom.xml
@@ -0,0 +1,46 @@
+
+
+
+ spring-cloud-kubernetes-integration-tests
+ org.springframework.cloud
+ 1.1.6.BUILD-SNAPSHOT
+
+ 4.0.0
+
+ Spring Cloud Kubernetes :: Integration Tests :: Load Balancer
+ load-balancer
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-kubernetes-loadbalancer
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ io.fabric8
+ kubernetes-server-mock
+ test
+
+
+ io.specto
+ hoverfly-java-junit5
+ test
+
+
+ io.specto
+ hoverfly-java
+ test
+
+
+
+
diff --git a/spring-cloud-kubernetes-integration-tests/load-balancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/SimpleLoadBalancerApp.java b/spring-cloud-kubernetes-integration-tests/load-balancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/SimpleLoadBalancerApp.java
new file mode 100644
index 00000000..0f91abd6
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/load-balancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/SimpleLoadBalancerApp.java
@@ -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";
+ }
+
+}
diff --git a/spring-cloud-kubernetes-integration-tests/load-balancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/LoadBalancerAllNamespacesTests.java b/spring-cloud-kubernetes-integration-tests/load-balancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/LoadBalancerAllNamespacesTests.java
new file mode 100644
index 00000000..29e87c79
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/load-balancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/LoadBalancerAllNamespacesTests.java
@@ -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();
+ }
+
+}
diff --git a/spring-cloud-kubernetes-integration-tests/load-balancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/LoadBalancerTests.java b/spring-cloud-kubernetes-integration-tests/load-balancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/LoadBalancerTests.java
new file mode 100644
index 00000000..41b01f68
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/load-balancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/LoadBalancerTests.java
@@ -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();
+ }
+
+}
diff --git a/spring-cloud-kubernetes-integration-tests/load-balancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/LoadBalancerWithServiceTests.java b/spring-cloud-kubernetes-integration-tests/load-balancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/LoadBalancerWithServiceTests.java
new file mode 100644
index 00000000..44a49670
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/load-balancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/LoadBalancerWithServiceTests.java
@@ -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();
+ }
+
+}
diff --git a/spring-cloud-kubernetes-integration-tests/pom.xml b/spring-cloud-kubernetes-integration-tests/pom.xml
index 096958db..09e0e171 100644
--- a/spring-cloud-kubernetes-integration-tests/pom.xml
+++ b/spring-cloud-kubernetes-integration-tests/pom.xml
@@ -144,7 +144,6 @@
simple-configmap
discovery
-
-
-
+ load-balancer
+
diff --git a/spring-cloud-kubernetes-loadbalancer/pom.xml b/spring-cloud-kubernetes-loadbalancer/pom.xml
new file mode 100644
index 00000000..3b4fcb3d
--- /dev/null
+++ b/spring-cloud-kubernetes-loadbalancer/pom.xml
@@ -0,0 +1,50 @@
+
+
+
+ org.springframework.cloud
+ spring-cloud-kubernetes
+ 1.1.6.BUILD-SNAPSHOT
+
+ 4.0.0
+
+ spring-cloud-kubernetes-loadbalancer
+ Spring Cloud Kubernetes :: Load Balancer
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-loadbalancer
+
+
+ org.springframework.cloud
+ spring-cloud-kubernetes-discovery
+
+
+ commons-lang
+ commons-lang
+ 2.6
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+ true
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ io.fabric8
+ kubernetes-server-mock
+ test
+
+
+
diff --git a/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerAutoConfiguration.java b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerAutoConfiguration.java
new file mode 100644
index 00000000..df6f0eb1
--- /dev/null
+++ b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerAutoConfiguration.java
@@ -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);
+ }
+
+}
diff --git a/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerClientConfiguration.java b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerClientConfiguration.java
new file mode 100644
index 00000000..4201f4c8
--- /dev/null
+++ b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerClientConfiguration.java
@@ -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);
+ }
+
+}
diff --git a/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerMode.java b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerMode.java
new file mode 100644
index 00000000..eaff0c08
--- /dev/null
+++ b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerMode.java
@@ -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
+
+}
diff --git a/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerProperties.java b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerProperties.java
new file mode 100644
index 00000000..1e9a3a15
--- /dev/null
+++ b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerProperties.java
@@ -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;
+ }
+
+}
diff --git a/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServiceInstanceMapper.java b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServiceInstanceMapper.java
new file mode 100644
index 00000000..cfe7bcef
--- /dev/null
+++ b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServiceInstanceMapper.java
@@ -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 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 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 getServiceMetadata(Service service) {
+ final Map serviceMetadata = new HashMap<>();
+ KubernetesDiscoveryProperties.Metadata metadataProps = this.discoveryProperties
+ .getMetadata();
+ if (metadataProps.isAddLabels()) {
+ Map labelMetadata = getMapWithPrefixedKeys(
+ service.getMetadata().getLabels(), metadataProps.getLabelsPrefix());
+ serviceMetadata.putAll(labelMetadata);
+ }
+ if (metadataProps.isAddAnnotations()) {
+ Map annotationMetadata = getMapWithPrefixedKeys(
+ service.getMetadata().getAnnotations(),
+ metadataProps.getAnnotationsPrefix());
+ serviceMetadata.putAll(annotationMetadata);
+ }
+
+ return serviceMetadata;
+ }
+
+ private Map getMapWithPrefixedKeys(Map map,
+ String prefix) {
+ if (map == null) {
+ return new HashMap<>();
+ }
+ if (!org.springframework.util.StringUtils.hasText(prefix)) {
+ return map;
+ }
+ final Map 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());
+ }
+
+}
diff --git a/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServicesListSupplier.java b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServicesListSupplier.java
new file mode 100644
index 00000000..63447b91
--- /dev/null
+++ b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServicesListSupplier.java
@@ -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> get() {
+ List result = new ArrayList<>();
+ if (discoveryProperties.isAllNamespaces()) {
+ List 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));
+ }
+
+}
diff --git a/spring-cloud-kubernetes-loadbalancer/src/main/resources/META-INF/spring.factories b/spring-cloud-kubernetes-loadbalancer/src/main/resources/META-INF/spring.factories
new file mode 100644
index 00000000..4a43c68f
--- /dev/null
+++ b/spring-cloud-kubernetes-loadbalancer/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+org.springframework.cloud.kubernetes.loadbalancer.KubernetesLoadBalancerAutoConfiguration
diff --git a/spring-cloud-kubernetes-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServiceInstanceMapperTests.java b/spring-cloud-kubernetes-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServiceInstanceMapperTests.java
new file mode 100644
index 00000000..c49d5187
--- /dev/null
+++ b/spring-cloud-kubernetes-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServiceInstanceMapperTests.java
@@ -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 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 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 ports,
+ Map 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 labels) {
+ ServicePort servicePort = new ServicePortBuilder().withPort(port)
+ .withName(portName).build();
+ return buildService(name, uid, Collections.singletonList(servicePort), labels);
+ }
+
+}
diff --git a/spring-cloud-kubernetes-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServiceListSupplierTests.java b/spring-cloud-kubernetes-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServiceListSupplierTests.java
new file mode 100644
index 00000000..9dd9c0ad
--- /dev/null
+++ b/spring-cloud-kubernetes-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServiceListSupplierTests.java
@@ -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> serviceOperation;
+
+ @Mock
+ NonNamespaceOperation> namespaceOperation;
+
+ @Mock
+ ServiceResource serviceResource;
+
+ @Mock
+ FilterWatchListMultiDeletable> 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 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 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();
+ }
+
+}
diff --git a/spring-cloud-starter-kubernetes-all/pom.xml b/spring-cloud-starter-kubernetes-all/pom.xml
index d39476bd..fd70d73b 100644
--- a/spring-cloud-starter-kubernetes-all/pom.xml
+++ b/spring-cloud-starter-kubernetes-all/pom.xml
@@ -51,6 +51,11 @@
spring-cloud-kubernetes-ribbon
+
+ org.springframework.cloud
+ spring-cloud-kubernetes-loadbalancer
+
+
diff --git a/spring-cloud-starter-kubernetes-loadbalancer/pom.xml b/spring-cloud-starter-kubernetes-loadbalancer/pom.xml
new file mode 100644
index 00000000..d252a071
--- /dev/null
+++ b/spring-cloud-starter-kubernetes-loadbalancer/pom.xml
@@ -0,0 +1,30 @@
+
+
+
+ spring-cloud-kubernetes
+ org.springframework.cloud
+ 1.1.6.BUILD-SNAPSHOT
+
+ 4.0.0
+
+ spring-cloud-starter-kubernetes-loadbalancer
+ Spring Cloud Kubernetes :: Starter :: LoadBalancer
+
+
+
+ org.springframework.cloud
+ spring-cloud-kubernetes-core
+
+
+ org.springframework.cloud
+ spring-cloud-kubernetes-loadbalancer
+
+
+ org.springframework.cloud
+ spring-cloud-starter-loadbalancer
+
+
+
+