Move loadbalancer code to commons (#688)

* Deprecate KubernetesAutoServiceRegistration

* Document service registry in kubernetes.  Fixes #348

* Move common load balancer code to spring-cloud-kubernetes-commons
This commit is contained in:
Ryan Baxter
2020-12-10 14:03:41 -05:00
committed by GitHub
parent 8a70e290e2
commit ea1a22f367
13 changed files with 179 additions and 93 deletions

View File

@@ -30,6 +30,10 @@
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.kubernetes.fabric8.loadbalancer;
package org.springframework.cloud.kubernetes.commons.loadbalancer;
/**
* Kubernetes load balancer mode enum.

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.kubernetes.fabric8.loadbalancer;
package org.springframework.cloud.kubernetes.commons.loadbalancer;
import org.springframework.boot.context.properties.ConfigurationProperties;

View File

@@ -0,0 +1,67 @@
/*
* 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.commons.loadbalancer;
import java.util.HashMap;
import java.util.Map;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesServiceInstance;
import org.springframework.util.StringUtils;
/**
* @author Ryan Baxter
*/
public interface KubernetesServiceInstanceMapper<T> {
KubernetesServiceInstance map(T service);
static String createHost(String serviceName, String namespace, String clusterDomain) {
return String.format("%s.%s.svc.%s", serviceName, StringUtils.hasText(namespace) ? namespace : "default",
clusterDomain);
}
static boolean isSecure(Map<String, String> labels, Map<String, String> annotations, String servicePortName,
Integer servicePort) {
if (labels != null) {
final String securedLabelValue = labels.getOrDefault("secured", "false");
if (securedLabelValue.equals("true")) {
return true;
}
}
if (annotations != null) {
final String securedAnnotationValue = annotations.getOrDefault("secured", "false");
if (securedAnnotationValue.equals("true")) {
return true;
}
}
return (servicePortName != null && servicePortName.endsWith("https")) || servicePort.toString().endsWith("443");
}
static Map<String, String> getMapWithPrefixedKeys(Map<String, String> map, String prefix) {
if (map == null) {
return new HashMap<>();
}
if (!StringUtils.hasText(prefix)) {
return map;
}
final Map<String, String> result = new HashMap<>();
map.forEach((k, v) -> result.put(prefix + k, v));
return result;
}
}

View File

@@ -0,0 +1,58 @@
/*
* 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.commons.loadbalancer;
import java.util.List;
import reactor.core.publisher.Flux;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.kubernetes.commons.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 abstract class KubernetesServicesListSupplier implements ServiceInstanceListSupplier {
protected final Environment environment;
protected final KubernetesDiscoveryProperties discoveryProperties;
protected final KubernetesServiceInstanceMapper mapper;
public KubernetesServicesListSupplier(Environment environment, KubernetesServiceInstanceMapper mapper,
KubernetesDiscoveryProperties discoveryProperties) {
this.environment = environment;
this.discoveryProperties = discoveryProperties;
this.mapper = mapper;
}
@Override
public String getServiceId() {
return environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
}
@Override
public abstract Flux<List<ServiceInstance>> get();
}

View File

@@ -20,6 +20,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.kubernetes.commons.ConditionalOnKubernetesEnabled;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesLoadBalancerProperties;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -33,13 +34,13 @@ import org.springframework.context.annotation.Configuration;
@EnableConfigurationProperties(KubernetesLoadBalancerProperties.class)
@ConditionalOnKubernetesEnabled
@ConditionalOnProperty(value = "spring.cloud.kubernetes.loadbalancer.enabled", matchIfMissing = true)
@LoadBalancerClients(defaultConfiguration = KubernetesLoadBalancerClientConfiguration.class)
public class KubernetesLoadBalancerAutoConfiguration {
@LoadBalancerClients(defaultConfiguration = Fabric8LoadBalancerClientConfiguration.class)
public class Fabric8LoadBalancerAutoConfiguration {
@Bean
KubernetesServiceInstanceMapper mapper(KubernetesLoadBalancerProperties properties,
Fabric8ServiceInstanceMapper mapper(KubernetesLoadBalancerProperties properties,
KubernetesDiscoveryProperties discoveryProperties) {
return new KubernetesServiceInstanceMapper(properties, discoveryProperties);
return new Fabric8ServiceInstanceMapper(properties, discoveryProperties);
}
}

View File

@@ -20,6 +20,7 @@ import io.fabric8.kubernetes.client.KubernetesClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServicesListSupplier;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
@@ -28,14 +29,14 @@ import org.springframework.core.env.Environment;
*
* @author Piotr Minkowski
*/
public class KubernetesLoadBalancerClientConfiguration {
public class Fabric8LoadBalancerClientConfiguration {
@Bean
@ConditionalOnProperty(name = "spring.cloud.kubernetes.loadbalancer.mode", havingValue = "SERVICE")
KubernetesServicesListSupplier kubernetesServicesListSupplier(Environment environment,
KubernetesClient kubernetesClient, KubernetesServiceInstanceMapper mapper,
KubernetesClient kubernetesClient, Fabric8ServiceInstanceMapper mapper,
KubernetesDiscoveryProperties discoveryProperties) {
return new KubernetesServicesListSupplier(environment, kubernetesClient, mapper, discoveryProperties);
return new Fabric8ServicesListSupplier(environment, kubernetesClient, mapper, discoveryProperties);
}
}

View File

@@ -25,28 +25,30 @@ 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.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesServiceInstance;
import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesLoadBalancerProperties;
import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServiceInstanceMapper;
/**
* Class for mapping Kubernetes Service object into {@link KubernetesServiceInstance}.
*
* @author Piotr Minkowski
*/
public class KubernetesServiceInstanceMapper {
public class Fabric8ServiceInstanceMapper implements KubernetesServiceInstanceMapper<Service> {
private final KubernetesLoadBalancerProperties properties;
private final KubernetesDiscoveryProperties discoveryProperties;
KubernetesServiceInstanceMapper(KubernetesLoadBalancerProperties properties,
Fabric8ServiceInstanceMapper(KubernetesLoadBalancerProperties properties,
KubernetesDiscoveryProperties discoveryProperties) {
this.properties = properties;
this.discoveryProperties = discoveryProperties;
}
@Override
public KubernetesServiceInstance map(Service service) {
final ObjectMeta meta = service.getMetadata();
final List<ServicePort> ports = service.getSpec().getPorts();
@@ -64,8 +66,10 @@ public class KubernetesServiceInstanceMapper {
if (port == null) {
return null;
}
final String host = createHost(service);
final boolean secure = isSecure(service, port);
final String host = KubernetesServiceInstanceMapper.createHost(service.getMetadata().getName(),
service.getMetadata().getNamespace(), properties.getClusterDomain());
final boolean secure = KubernetesServiceInstanceMapper.isSecure(service.getMetadata().getLabels(),
service.getMetadata().getAnnotations(), port.getName(), port.getPort());
return new KubernetesServiceInstance(meta.getUid(), meta.getName(), host, port.getPort(),
getServiceMetadata(service), secure);
}
@@ -74,55 +78,17 @@ public class KubernetesServiceInstanceMapper {
final Map<String, String> serviceMetadata = new HashMap<>();
KubernetesDiscoveryProperties.Metadata metadataProps = this.discoveryProperties.getMetadata();
if (metadataProps.isAddLabels()) {
Map<String, String> labelMetadata = getMapWithPrefixedKeys(service.getMetadata().getLabels(),
metadataProps.getLabelsPrefix());
Map<String, String> labelMetadata = KubernetesServiceInstanceMapper
.getMapWithPrefixedKeys(service.getMetadata().getLabels(), metadataProps.getLabelsPrefix());
serviceMetadata.putAll(labelMetadata);
}
if (metadataProps.isAddAnnotations()) {
Map<String, String> annotationMetadata = getMapWithPrefixedKeys(service.getMetadata().getAnnotations(),
metadataProps.getAnnotationsPrefix());
Map<String, String> annotationMetadata = KubernetesServiceInstanceMapper.getMapWithPrefixedKeys(
service.getMetadata().getAnnotations(), metadataProps.getAnnotationsPrefix());
serviceMetadata.putAll(annotationMetadata);
}
return serviceMetadata;
}
private Map<String, String> getMapWithPrefixedKeys(Map<String, String> map, String prefix) {
if (map == null) {
return new HashMap<>();
}
if (!org.springframework.util.StringUtils.hasText(prefix)) {
return map;
}
final Map<String, String> result = new HashMap<>();
map.forEach((k, v) -> result.put(prefix + k, v));
return result;
}
private boolean isSecure(Service service, ServicePort port) {
if (service.getMetadata().getLabels() != null) {
final String securedLabelValue = service.getMetadata().getLabels().getOrDefault("secured", "false");
if (securedLabelValue.equals("true")) {
return true;
}
}
if (service.getMetadata().getAnnotations() != null) {
final String securedAnnotationValue = service.getMetadata().getAnnotations().getOrDefault("secured",
"false");
if (securedAnnotationValue.equals("true")) {
return true;
}
}
return (port.getName() != null && port.getName().endsWith("https"))
|| port.getPort().toString().endsWith("443");
}
private String createHost(Service service) {
return String.format("%s.%s.svc.%s", service.getMetadata().getName(),
StringUtils.isNotBlank(service.getMetadata().getNamespace()) ? service.getMetadata().getNamespace()
: "default",
properties.getClusterDomain());
}
}

View File

@@ -21,14 +21,14 @@ 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.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServicesListSupplier;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
/**
* Implementation of {@link ServiceInstanceListSupplier} for load balancer in SERVICE
@@ -36,27 +36,14 @@ import org.springframework.core.env.Environment;
*
* @author Piotr Minkowski
*/
public class KubernetesServicesListSupplier implements ServiceInstanceListSupplier {
private final Environment environment;
public class Fabric8ServicesListSupplier extends KubernetesServicesListSupplier {
private final KubernetesClient kubernetesClient;
private final KubernetesDiscoveryProperties discoveryProperties;
private final KubernetesServiceInstanceMapper mapper;
KubernetesServicesListSupplier(Environment environment, KubernetesClient kubernetesClient,
KubernetesServiceInstanceMapper mapper, KubernetesDiscoveryProperties discoveryProperties) {
this.environment = environment;
Fabric8ServicesListSupplier(Environment environment, KubernetesClient kubernetesClient,
Fabric8ServiceInstanceMapper mapper, KubernetesDiscoveryProperties discoveryProperties) {
super(environment, mapper, discoveryProperties);
this.kubernetesClient = kubernetesClient;
this.discoveryProperties = discoveryProperties;
this.mapper = mapper;
}
@Override
public String getServiceId() {
return environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
}
@Override
@@ -68,7 +55,7 @@ public class KubernetesServicesListSupplier implements ServiceInstanceListSuppli
services.forEach(service -> result.add(mapper.map(service)));
}
else {
Service service = StringUtils.isNotBlank(this.kubernetesClient.getNamespace())
Service service = StringUtils.hasText(this.kubernetesClient.getNamespace())
? this.kubernetesClient.services().inNamespace(this.kubernetesClient.getNamespace())
.withName(this.getServiceId()).get()
: this.kubernetesClient.services().withName(this.getServiceId()).get();

View File

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

View File

@@ -28,7 +28,7 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Thomas Vitale
*/
class KubernetesLoadBalancerAutoConfigurationTests {
class Fabric8LoadBalancerAutoConfigurationTests {
private ConfigurableApplicationContext context;
@@ -42,35 +42,35 @@ class KubernetesLoadBalancerAutoConfigurationTests {
@Test
void kubernetesLoadBalancerWhenKubernetesDisabledAndLoadBalancerDisabled() {
setup("spring.cloud.kubernetes.enabled=false", "spring.cloud.kubernetes.loadbalancer.enabled=false");
assertThat(this.context.getBeanNamesForType(KubernetesServiceInstanceMapper.class)).isEmpty();
assertThat(this.context.getBeanNamesForType(Fabric8ServiceInstanceMapper.class)).isEmpty();
}
@Test
void kubernetesLoadBalancerWhenKubernetesDisabledAndLoadBalancerEnabled() {
setup("spring.cloud.kubernetes.enabled=false", "spring.cloud.kubernetes.loadbalancer.enabled=true");
assertThat(this.context.getBeanNamesForType(KubernetesServiceInstanceMapper.class)).isEmpty();
assertThat(this.context.getBeanNamesForType(Fabric8ServiceInstanceMapper.class)).isEmpty();
}
@Test
void kubernetesLoadBalancerWhenKubernetesEnabledAndLoadBalancerEnabled() {
setup("spring.cloud.kubernetes.enabled=true", "spring.cloud.kubernetes.loadbalancer.enabled=true");
assertThat(this.context.getBeanNamesForType(KubernetesServiceInstanceMapper.class)).hasSize(1);
assertThat(this.context.getBeanNamesForType(Fabric8ServiceInstanceMapper.class)).hasSize(1);
}
@Test
void kubernetesLoadBalancerWhenKubernetesEnabledAndLoadBalancerDisabled() {
setup("spring.cloud.kubernetes.enabled=true", "spring.cloud.kubernetes.loadbalancer.enabled=false");
assertThat(this.context.getBeanNamesForType(KubernetesServiceInstanceMapper.class)).isEmpty();
assertThat(this.context.getBeanNamesForType(Fabric8ServiceInstanceMapper.class)).isEmpty();
}
@Test
void kubernetesLoadBalancerWhenDefaultProperties() {
setup();
assertThat(this.context.getBeanNamesForType(KubernetesServiceInstanceMapper.class)).hasSize(1);
assertThat(this.context.getBeanNamesForType(Fabric8ServiceInstanceMapper.class)).hasSize(1);
}
private void setup(String... env) {
this.context = new SpringApplicationBuilder(KubernetesLoadBalancerAutoConfiguration.class,
this.context = new SpringApplicationBuilder(Fabric8LoadBalancerAutoConfiguration.class,
KubernetesDiscoveryProperties.class).web(org.springframework.boot.WebApplicationType.NONE)
.properties(env).run();
}

View File

@@ -31,15 +31,16 @@ import org.junit.jupiter.api.Test;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesServiceInstance;
import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesLoadBalancerProperties;
class KubernetesServiceInstanceMapperTests {
class Fabric8ServiceInstanceMapperTests {
@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)
KubernetesServiceInstance instance = new Fabric8ServiceInstanceMapper(properties, discoveryProperties)
.map(service);
Assertions.assertNotNull(instance);
Assertions.assertEquals("test", instance.getServiceId());
@@ -55,7 +56,7 @@ class KubernetesServiceInstanceMapperTests {
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)
KubernetesServiceInstance instance = new Fabric8ServiceInstanceMapper(properties, discoveryProperties)
.map(service);
Assertions.assertNotNull(instance);
Assertions.assertEquals("test", instance.getServiceId());
@@ -68,7 +69,7 @@ class KubernetesServiceInstanceMapperTests {
KubernetesLoadBalancerProperties properties = new KubernetesLoadBalancerProperties();
KubernetesDiscoveryProperties discoveryProperties = new KubernetesDiscoveryProperties();
Service service = buildService("test", "abc", 443, null, new HashMap<>());
KubernetesServiceInstance instance = new KubernetesServiceInstanceMapper(properties, discoveryProperties)
KubernetesServiceInstance instance = new Fabric8ServiceInstanceMapper(properties, discoveryProperties)
.map(service);
Assertions.assertNotNull(instance);
Assertions.assertEquals("test", instance.getServiceId());
@@ -83,7 +84,7 @@ class KubernetesServiceInstanceMapperTests {
List<ServicePort> ports = new ArrayList<>();
ports.add(new ServicePortBuilder().withPort(443).build());
Service service = buildService("test", "abc", ports, null, null);
KubernetesServiceInstance instance = new KubernetesServiceInstanceMapper(properties, discoveryProperties)
KubernetesServiceInstance instance = new Fabric8ServiceInstanceMapper(properties, discoveryProperties)
.map(service);
Assertions.assertNotNull(instance);
Assertions.assertEquals("test", instance.getServiceId());
@@ -99,7 +100,7 @@ class KubernetesServiceInstanceMapperTests {
labels.put("secured", "true");
labels.put("label1", "123");
Service service = buildService("test", "abc", 8080, null, labels);
KubernetesServiceInstance instance = new KubernetesServiceInstanceMapper(properties, discoveryProperties)
KubernetesServiceInstance instance = new Fabric8ServiceInstanceMapper(properties, discoveryProperties)
.map(service);
Assertions.assertNotNull(instance);
Assertions.assertEquals("test", instance.getServiceId());

View File

@@ -38,6 +38,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesServiceInstance;
import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServicesListSupplier;
import org.springframework.core.env.Environment;
import static org.mockito.ArgumentMatchers.any;
@@ -50,7 +51,7 @@ class KubernetesServiceListSupplierTests {
Environment environment;
@Mock
KubernetesServiceInstanceMapper mapper;
Fabric8ServiceInstanceMapper mapper;
@Mock
KubernetesClient client;
@@ -76,7 +77,7 @@ class KubernetesServiceListSupplierTests {
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,
KubernetesServicesListSupplier supplier = new Fabric8ServicesListSupplier(environment, client, mapper,
new KubernetesDiscoveryProperties());
List<ServiceInstance> instances = supplier.get().blockFirst();
assert instances != null;
@@ -95,7 +96,7 @@ class KubernetesServiceListSupplierTests {
when(this.multiDeletable.list()).thenReturn(serviceList);
KubernetesDiscoveryProperties discoveryProperties = new KubernetesDiscoveryProperties();
discoveryProperties.setAllNamespaces(true);
KubernetesServicesListSupplier supplier = new KubernetesServicesListSupplier(environment, client, mapper,
KubernetesServicesListSupplier supplier = new Fabric8ServicesListSupplier(environment, client, mapper,
discoveryProperties);
List<ServiceInstance> instances = supplier.get().blockFirst();
assert instances != null;