diff --git a/pom.xml b/pom.xml index 9f326c33..4ec0d59d 100644 --- a/pom.xml +++ b/pom.xml @@ -96,6 +96,7 @@ spring-cloud-kubernetes-istio spring-cloud-kubernetes-integration-tests docs + spring-cloud-kubernetes-loadbalancer diff --git a/spring-cloud-kubernetes-loadbalancer/pom.xml b/spring-cloud-kubernetes-loadbalancer/pom.xml new file mode 100644 index 00000000..93514763 --- /dev/null +++ b/spring-cloud-kubernetes-loadbalancer/pom.xml @@ -0,0 +1,45 @@ + + + + spring-cloud-kubernetes + org.springframework.cloud + 2.0.0-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 + + + diff --git a/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesClientConfiguration.java b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesClientConfiguration.java new file mode 100644 index 00000000..dddc13f4 --- /dev/null +++ b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesClientConfiguration.java @@ -0,0 +1,40 @@ +/* + * 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.context.annotation.Bean; +import org.springframework.core.env.Environment; + +/** + * Kubernetes load balancer client properties. + * + * @author Piotr Minkowski + */ +public class KubernetesClientConfiguration { + + @Bean + @ConditionalOnProperty(name = "spring.cloud.kubernetes.loadbalancer.mode", + havingValue = "SERVICE") + KubernetesServicesListSupplier kubernetesServicesListSupplier(Environment environment, + KubernetesClient kubernetesClient, KubernetesServiceInstanceMapper mapper) { + return new KubernetesServicesListSupplier(environment, kubernetesClient, mapper); + } + +} 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..5d388db9 --- /dev/null +++ b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerAutoConfiguration.java @@ -0,0 +1,42 @@ +/* + * 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.cloud.kubernetes.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Piotr Minkowski + */ +@Configuration +@LoadBalancerClients(defaultConfiguration = KubernetesClientConfiguration.class) +public class KubernetesLoadBalancerAutoConfiguration { + + @Bean + KubernetesLoadBalancerProperties properties() { + return new KubernetesLoadBalancerProperties(); + } + + @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/KubernetesLoadBalancerMode.java b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerMode.java new file mode 100644 index 00000000..41eb66c4 --- /dev/null +++ b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerMode.java @@ -0,0 +1,33 @@ +/* + * 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; + +/** + * @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..88e27152 --- /dev/null +++ b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerProperties.java @@ -0,0 +1,112 @@ +/* + * 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; + +/** + * @author Piotr Minkowski + */ +@ConfigurationProperties(prefix = "spring.cloud.kubernetes.loadbalancer") +public class KubernetesLoadBalancerProperties { + + /** + * Ribbon enabled,default true. + */ + private Boolean enabled = true; + + /** + * {@link KubernetesLoadBalancerMode} setting ribbon 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/KubernetesServiceInstance.java b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServiceInstance.java new file mode 100644 index 00000000..dcf71944 --- /dev/null +++ b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServiceInstance.java @@ -0,0 +1,84 @@ +/* + * 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.net.URI; +import java.util.Map; + +import org.springframework.cloud.client.ServiceInstance; + +/** + * @author Piotr Minkowski + */ +public class KubernetesServiceInstance implements ServiceInstance { + + private String serviceId; + + private String instanceId; + + private int port; + + private boolean secure; + + private String host; + + private URI uri; + + private Map metadata; + + KubernetesServiceInstance(String serviceId, String instanceId, int port, + boolean secure, String host, URI uri, Map metadata) { + this.serviceId = serviceId; + this.instanceId = instanceId; + this.port = port; + this.secure = secure; + this.host = host; + this.uri = uri; + this.metadata = metadata; + } + + @Override + public String getServiceId() { + return serviceId; + } + + @Override + public String getHost() { + return host; + } + + @Override + public int getPort() { + return port; + } + + @Override + public boolean isSecure() { + return secure; + } + + @Override + public URI getUri() { + return uri; + } + + @Override + public Map getMetadata() { + return metadata; + } + +} 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..a54baf5f --- /dev/null +++ b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServiceInstanceMapper.java @@ -0,0 +1,140 @@ +/* + * 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.net.URI; +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; + +/** + * @author Piotr Minkowski + */ +public class KubernetesServiceInstanceMapper { + + private static final String DSL = "//"; + + private static final String COLON = ":"; + + 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.getName(), meta.getUid(), + port.getPort(), secure, host, + createUri(secure ? "https" : "http", host, port.getPort()), + getServiceMetadata(service)); + } + + 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()); + } + + 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-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..51fe8fbc --- /dev/null +++ b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServicesListSupplier.java @@ -0,0 +1,69 @@ +/* + * 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.loadbalancer.core.ServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; +import org.springframework.core.env.Environment; + +/** + * @author Piotr Minkowski + */ +public class KubernetesServicesListSupplier implements ServiceInstanceListSupplier { + + private Environment environment; + + private KubernetesClient kubernetesClient; + + private KubernetesServiceInstanceMapper mapper; + + KubernetesServicesListSupplier(Environment environment, + KubernetesClient kubernetesClient, KubernetesServiceInstanceMapper mapper) { + this.environment = environment; + this.kubernetesClient = kubernetesClient; + this.mapper = mapper; + } + + @Override + public String getServiceId() { + return environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); + } + + @Override + public Flux> get() { + List result = new ArrayList<>(); + 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.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