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