From 6bfcf69834bf776bc65349bec436fd27cd16edc6 Mon Sep 17 00:00:00 2001 From: erabii Date: Sun, 7 Apr 2024 16:37:04 +0300 Subject: [PATCH 1/2] Lb both clients fix issue (#1628) --- ...KubernetesClientServiceInstanceMapper.java | 9 +++++---- ...netesClientServiceInstanceMapperTests.java | 19 +++++++++++++++++++ .../KubernetesDiscoveryConstants.java | 5 +++++ .../Fabric8ServiceInstanceMapper.java | 9 +++++---- .../Fabric8ServiceInstanceMapperTests.java | 15 +++++++-------- 5 files changed, 41 insertions(+), 16 deletions(-) diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientServiceInstanceMapper.java b/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientServiceInstanceMapper.java index 6c30cde8c1..249739c868 100644 --- a/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientServiceInstanceMapper.java +++ b/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientServiceInstanceMapper.java @@ -40,6 +40,7 @@ import org.springframework.util.StringUtils; import static java.util.Optional.ofNullable; +import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.NON_DETERMINISTIC_PORT_MESSAGE; import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.PORT_NAME_PROPERTY; import static org.springframework.cloud.kubernetes.commons.discovery.ServicePortSecureResolver.Input; @@ -102,9 +103,9 @@ public KubernetesServiceInstance map(V1Service service) { } } else { - LOG.warn(() -> PORT_NAME_PROPERTY + " is not set, as such will not consider service with name : " - + metadata.getName()); - return null; + LOG.warn(() -> PORT_NAME_PROPERTY + " is not set"); + LOG.warn(() -> NON_DETERMINISTIC_PORT_MESSAGE); + port = ports.get(0); } } @@ -135,7 +136,7 @@ private boolean secure(V1ServicePort port, V1Service service) { private void logWarning(String portNameFromProperties) { LOG.warn(() -> "Did not find a port name that is equal to the value " + portNameFromProperties); - LOG.warn(() -> "Will return 'first' port found, which is non-deterministic"); + LOG.warn(() -> NON_DETERMINISTIC_PORT_MESSAGE); } } diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientServiceInstanceMapperTests.java b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientServiceInstanceMapperTests.java index 885ee7e5e4..950f3c278f 100644 --- a/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientServiceInstanceMapperTests.java +++ b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientServiceInstanceMapperTests.java @@ -192,6 +192,25 @@ void multiplePortsNameDoesNotMatchProperty(CapturedOutput output) { Assertions.assertTrue(serviceInstance.getPort() == 80 || serviceInstance.getPort() == 443); } + @Test + void multiPortsEmptyPortNameProperty(CapturedOutput output) { + KubernetesLoadBalancerProperties loadBalancerProperties = new KubernetesLoadBalancerProperties(); + loadBalancerProperties.setPortName(""); + KubernetesClientServiceInstanceMapper mapper = new KubernetesClientServiceInstanceMapper(loadBalancerProperties, + KubernetesDiscoveryProperties.DEFAULT); + + Map annotations = Map.of("org.springframework.cloud", "true"); + Map labels = Map.of("beta", "true"); + List servicePorts = List.of(new V1ServicePortBuilder().withName("http-api").withPort(80).build(), + new V1ServicePortBuilder().withName("https").withPort(443).build()); + V1Service service = createService("database", "default", annotations, labels, servicePorts); + KubernetesServiceInstance serviceInstance = mapper.map(service); + Assertions.assertNotNull(serviceInstance); + Assertions.assertTrue(output.getOut().contains("'spring.cloud.kubernetes.loadbalancer.portName' is not set")); + Assertions.assertTrue(output.getOut().contains("Will return 'first' port found, which is non-deterministic")); + Assertions.assertTrue(serviceInstance.getPort() == 80 || serviceInstance.getPort() == 443); + } + private V1Service createService(String name, String namespace, Map annotations, Map labels, List servicePorts) { return new V1ServiceBuilder() diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryConstants.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryConstants.java index 5177b4c227..8ebcd23fea 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryConstants.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryConstants.java @@ -103,4 +103,9 @@ private KubernetesDiscoveryConstants() { */ public static final String PORT_NAME_PROPERTY = "'spring.cloud.kubernetes.loadbalancer.portName'"; + /** + * message for non-deterministic port. + */ + public static final String NON_DETERMINISTIC_PORT_MESSAGE = "Will return 'first' port found, which is non-deterministic"; + } diff --git a/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceInstanceMapper.java b/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceInstanceMapper.java index 6f44a85ce0..7cccfafcc8 100644 --- a/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceInstanceMapper.java +++ b/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceInstanceMapper.java @@ -39,6 +39,7 @@ import org.springframework.core.log.LogAccessor; import org.springframework.util.StringUtils; +import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.NON_DETERMINISTIC_PORT_MESSAGE; import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.PORT_NAME_PROPERTY; import static org.springframework.cloud.kubernetes.commons.discovery.ServicePortSecureResolver.Input; @@ -101,9 +102,9 @@ public KubernetesServiceInstance map(Service service) { } } else { - LOG.warn(() -> PORT_NAME_PROPERTY + " is not set, as such will not consider service with name : " - + metadata.getName()); - return null; + LOG.warn(() -> PORT_NAME_PROPERTY + " is not set"); + LOG.warn(() -> NON_DETERMINISTIC_PORT_MESSAGE); + port = ports.get(0); } } @@ -130,7 +131,7 @@ private boolean secure(ServicePort port, Service service) { private void logWarning(String portNameFromProperties) { LOG.warn(() -> "Did not find a port name that is equal to the value " + portNameFromProperties); - LOG.warn(() -> "Will return 'first' port found, which is non-deterministic"); + LOG.warn(() -> NON_DETERMINISTIC_PORT_MESSAGE); } } diff --git a/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceInstanceMapperTests.java b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceInstanceMapperTests.java index 04ea207dfc..95c5725538 100644 --- a/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceInstanceMapperTests.java +++ b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceInstanceMapperTests.java @@ -217,7 +217,7 @@ void testSinglePortDoesNotMatchProperty(CapturedOutput output) { /** *
 	 *     service has multiple ServicePorts, and 'spring.cloud.kubernetes.loadbalancer.portName' is empty.
-	 *     in this case, service will be skipped.
+	 *     in this case, a single, 'first', port will be returned.
 	 * 
*/ @Test @@ -234,16 +234,15 @@ void testMultiplePortsWithoutPortNameProperty(CapturedOutput output) { KubernetesServiceInstance result = new Fabric8ServiceInstanceMapper(loadBalancerProperties, discoveryProperties) .map(service); - Assertions.assertNull(result); - Assertions.assertTrue(output.getOut().contains( - "'spring.cloud.kubernetes.loadbalancer.portName' is not set, as such will not consider service with name : test")); + Assertions.assertNotNull(result); + Assertions.assertTrue(output.getOut().contains("'spring.cloud.kubernetes.loadbalancer.portName' is not set")); + Assertions.assertTrue(output.getOut().contains("Will return 'first' port found, which is non-deterministic")); } /** *
-	 *     service has multiple ServicePorts, and 'spring.cloud.kubernetes.loadbalancer.portName' is empty.
-	 *     in this case, service will be skipped.
+	 *     service has multiple ServicePorts, and 'spring.cloud.kubernetes.loadbalancer.portName' is not empty.
 	 * 
*/ @Test @@ -267,8 +266,8 @@ void testMultiplePortsWithPortNamePropertyMatch(CapturedOutput output) { /** *
-	 *     service has multiple ServicePorts, and 'spring.cloud.kubernetes.loadbalancer.portName' is empty.
-	 *     in this case, service will be skipped.
+	 *     service has multiple ServicePorts, and 'spring.cloud.kubernetes.loadbalancer.portName' is not empty.
+	 *     property name also does not match 'potName'
 	 * 
*/ @Test From 5009d6f21b35b038939c1d3e629885b8ffe3bb24 Mon Sep 17 00:00:00 2001 From: erabii Date: Sun, 7 Apr 2024 22:07:43 +0300 Subject: [PATCH 2/2] Loadbalancer documentation (#1630) --- docs/modules/ROOT/pages/load-balancer.adoc | 65 ++++++++++++++++++++-- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/docs/modules/ROOT/pages/load-balancer.adoc b/docs/modules/ROOT/pages/load-balancer.adoc index ea1e2ea55a..c7b8144aa7 100644 --- a/docs/modules/ROOT/pages/load-balancer.adoc +++ b/docs/modules/ROOT/pages/load-balancer.adoc @@ -1,7 +1,7 @@ [[loadbalancer-for-kubernetes]] = 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. +This project includes Spring Cloud Load Balancer for load balancing based on either Kubernetes Endpoints or Kubernetes Service. To include it to your project add the following dependency. Fabric8 Implementation [source,xml] @@ -21,16 +21,71 @@ Kubernetes Java Client Implementation ---- -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` +There are two "modes" in which load balancer works: `POD` and `SERVICE`, denoted by the property (default being `POD`) + [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. +or + +[source] +---- +spring.cloud.kubernetes.loadbalancer.mode=POD +---- + +In `POD` mode, we will use the `DiscoveryClient` to find all services that match your load balancer name. For example, if you have a configuration like this: + +[source] +---- +@Bean +@LoadBalanced +WebClient.Builder client() { + return WebClient.builder(); +} +---- + +and issue a request to `http://service-a` using that `WebClient`, we will use `service-a` to call `DiscoveryClient::getInstances` with this value. Since this is using `DiscoveryClient`, all the configuration specific to it apply, which are explained in the relevant part of the documentation. + +On the other hand, if you use `SERVICE` mode, things are a bit different, but closely resemble discovery client settings. For example, to answer the question in which namespace(s) to look for service(s) with name `service-a`, we will use one of the settings: + [source] ---- -spring.cloud.kubernetes.discovery.all-namespaces=true +spring.cloud.kubernetes.discovery.all-namespaces +spring.cloud.kubernetes.discovery.namespaces ---- -If a service needs to be accessed over HTTPS you need to add a label or annotation to your service definition with the name `secured` and the value `true` and the load balancer will then use HTTPS to make requests to the service. +to either search in all-namespaces, or the so-called "selective namespaces". If none of the above are specified, xref:property-source-config.adoc#namespace-resolution[Namespace Resolution] kicks in. + +Once we find all the services, we need to know what port to call them by. If the service in question has a single port defined, that is what we will use, no matter of its name. If there are no ports defined, this service will not be considered for load balancing and will be skipped. + +If there are more then one ports defined, we will try to match its name to the value of the property (`http` by default): + +[source] +---- +spring.cloud.kubernetes.loadbalancer.portName +---- + +In case such a match is found, that port number will be used. Otherwise, the "first" port from the list will be used. This last option is non-deterministic and care must be taken. + +Once we know the port, we know how to call that service. The URL will have the form: + +[source] +---- +service-a..svc.: +---- + + +`` is the namespace where the service resides, `DOMAIN` is the value of the property (by default it is equal to `cluster.local`): + +[source] +---- +spring.cloud.kubernetes.loadbalancer.clusterDomain +---- + +and `` is the port of the service that we have chosen described in the process above. + +If a service needs to be accessed over HTTPS, you need to explicitly configure that. The rules for that are exactly the same as for the discovery implementation and can be found in the relevant part of the documentation regarding discovery-client. + +