diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesClientServicesFunction.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesClientServicesFunction.java new file mode 100644 index 00000000..daa6b836 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesClientServicesFunction.java @@ -0,0 +1,45 @@ +/* + * Copyright 2013-2018 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 + * + * http://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.discovery; + +import io.fabric8.kubernetes.api.model.Service; +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.FilterWatchListDeletable; + +import java.util.function.Function; + +/** + * A regular java.util.function that is used to hide the complexity of the KubernetesClient interfaces + * + * It's meant to be used to abstract things like: + * + * client.services() + * client.services().withLabel("key", "value") + * client.services().withoutLabel("key") + * + * The result of the application of the function can then be used for example to list the services like so: + * + * function.apply(client).list() + * + * See KubernetesDiscoveryClientAutoConfiguration.servicesFunction + */ +public interface KubernetesClientServicesFunction extends + Function>> { +} 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 7fdf024d..5d1d0bbf 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 @@ -16,13 +16,6 @@ */ package org.springframework.cloud.kubernetes.discovery; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Predicate; -import java.util.stream.Collectors; - import io.fabric8.kubernetes.api.model.EndpointAddress; import io.fabric8.kubernetes.api.model.EndpointPort; import io.fabric8.kubernetes.api.model.EndpointSubset; @@ -31,7 +24,6 @@ import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.client.KubernetesClient; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.expression.Expression; @@ -40,6 +32,13 @@ import org.springframework.expression.spel.support.SimpleEvaluationContext; import org.springframework.util.Assert; import org.springframework.util.StringUtils; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; +import java.util.stream.Collectors; + import static java.util.stream.Collectors.toMap; public class KubernetesDiscoveryClient implements DiscoveryClient { @@ -50,6 +49,7 @@ public class KubernetesDiscoveryClient implements DiscoveryClient { private final KubernetesDiscoveryProperties properties; private final DefaultIsServicePortSecureResolver isServicePortSecureResolver; + private final KubernetesClientServicesFunction kubernetesClientServicesFunction; private final SpelExpressionParser parser = new SpelExpressionParser(); private final SimpleEvaluationContext evalCtxt = SimpleEvaluationContext .forReadOnlyDataBinding() @@ -57,17 +57,21 @@ public class KubernetesDiscoveryClient implements DiscoveryClient { .build(); public KubernetesDiscoveryClient(KubernetesClient client, - KubernetesDiscoveryProperties kubernetesDiscoveryProperties) { + KubernetesDiscoveryProperties kubernetesDiscoveryProperties, + KubernetesClientServicesFunction kubernetesClientServicesFunction) { - this(client, kubernetesDiscoveryProperties, new DefaultIsServicePortSecureResolver(kubernetesDiscoveryProperties)); + this(client, kubernetesDiscoveryProperties, kubernetesClientServicesFunction, + new DefaultIsServicePortSecureResolver(kubernetesDiscoveryProperties)); } KubernetesDiscoveryClient(KubernetesClient client, - KubernetesDiscoveryProperties kubernetesDiscoveryProperties, - DefaultIsServicePortSecureResolver isServicePortSecureResolver) { + KubernetesDiscoveryProperties kubernetesDiscoveryProperties, + KubernetesClientServicesFunction kubernetesClientServicesFunction, + DefaultIsServicePortSecureResolver isServicePortSecureResolver) { this.client = client; this.properties = kubernetesDiscoveryProperties; + this.kubernetesClientServicesFunction = kubernetesClientServicesFunction; this.isServicePortSecureResolver = isServicePortSecureResolver; } @@ -191,7 +195,7 @@ public class KubernetesDiscoveryClient implements DiscoveryClient { } public List getServices(Predicate filter) { - return client.services().list() + return kubernetesClientServicesFunction.apply(client).list() .getItems() .stream() .filter(filter) diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientAutoConfiguration.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientAutoConfiguration.java index 45adcb40..ac8d7bf3 100644 --- a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientAutoConfiguration.java +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientAutoConfiguration.java @@ -40,13 +40,23 @@ public class KubernetesDiscoveryClientAutoConfiguration { return new DefaultIsServicePortSecureResolver(properties); } + @Bean + public KubernetesClientServicesFunction servicesFunction(KubernetesDiscoveryProperties properties) { + if (properties.getServiceLabels().isEmpty()) { + return KubernetesClient::services; + } + + return (client) -> client.services().withLabels(properties.getServiceLabels()); + } + @Bean @ConditionalOnMissingBean @ConditionalOnProperty(name = "spring.cloud.kubernetes.discovery.enabled",matchIfMissing = true) - public KubernetesDiscoveryClient kubernetesDiscoveryClient( - KubernetesClient client, KubernetesDiscoveryProperties properties, - DefaultIsServicePortSecureResolver isServicePortSecureResolver) { - return new KubernetesDiscoveryClient(client, properties, isServicePortSecureResolver); + public KubernetesDiscoveryClient kubernetesDiscoveryClient(KubernetesClient client, + KubernetesDiscoveryProperties properties, + KubernetesClientServicesFunction kubernetesClientServicesFunction, + DefaultIsServicePortSecureResolver isServicePortSecureResolver) { + return new KubernetesDiscoveryClient(client, properties, kubernetesClientServicesFunction, isServicePortSecureResolver); } @Bean diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryProperties.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryProperties.java index 55692b8b..961b385a 100644 --- a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryProperties.java +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryProperties.java @@ -21,9 +21,9 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.core.style.ToStringCreator; -import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; -import java.util.List; +import java.util.Map; import java.util.Set; @ConfigurationProperties("spring.cloud.kubernetes.discovery") @@ -36,7 +36,7 @@ public class KubernetesDiscoveryProperties { @Value("${spring.application.name:unknown}") private String serviceName = "unknown"; - /** SpEL expression to filter services. */ + /** SpEL expression to filter services AFTER they have been retrieved from the Kubernetes API server. */ private String filter; @@ -46,6 +46,9 @@ public class KubernetesDiscoveryProperties { add(8443); }}; + /** If set, then only the services matching these labels will be fetched from the Kubernetes API server */ + private Map serviceLabels = new HashMap<>(); + private Metadata metadata = new Metadata(); public boolean isEnabled() { @@ -80,6 +83,14 @@ public class KubernetesDiscoveryProperties { this.knownSecurePorts = knownSecurePorts; } + public Map getServiceLabels() { + return serviceLabels; + } + + public void setServiceLabels(Map serviceLabels) { + this.serviceLabels = serviceLabels; + } + public Metadata getMetadata() { return metadata; } @@ -94,6 +105,8 @@ public class KubernetesDiscoveryProperties { .append("enabled", enabled) .append("serviceName", serviceName) .append("filter", filter) + .append("knownSecurePorts", knownSecurePorts) + .append("serviceLabels", serviceLabels) .append("metadata", metadata) .toString(); } diff --git a/spring-cloud-kubernetes-discovery/src/test/groovy/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientTest.groovy b/spring-cloud-kubernetes-discovery/src/test/groovy/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientTest.groovy index f786a787..b0d566ef 100644 --- a/spring-cloud-kubernetes-discovery/src/test/groovy/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientTest.groovy +++ b/spring-cloud-kubernetes-discovery/src/test/groovy/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientTest.groovy @@ -19,6 +19,7 @@ package org.springframework.cloud.kubernetes.discovery import io.fabric8.kubernetes.api.model.EndpointsBuilder import io.fabric8.kubernetes.api.model.ServiceBuilder +import io.fabric8.kubernetes.api.model.ServiceListBuilder import io.fabric8.kubernetes.client.Config import io.fabric8.kubernetes.client.KubernetesClient import io.fabric8.kubernetes.server.mock.KubernetesMockServer @@ -26,6 +27,8 @@ import org.springframework.cloud.client.ServiceInstance import org.springframework.cloud.client.discovery.DiscoveryClient import spock.lang.Specification +import static org.assertj.core.api.Assertions.assertThat + class KubernetesDiscoveryClientTest extends Specification { private static KubernetesMockServer mockServer = new KubernetesMockServer() @@ -47,7 +50,7 @@ class KubernetesDiscoveryClientTest extends Specification { mockServer.destroy() } - def "Should be able to handle endpoints single address"() { + def "getInstances should be able to handle endpoints single address"() { given: mockServer.expect().get().withPath("/api/v1/namespaces/test/endpoints/endpoint").andReturn(200, new EndpointsBuilder() .withNewMetadata() @@ -73,7 +76,7 @@ class KubernetesDiscoveryClientTest extends Specification { final properties = new KubernetesDiscoveryProperties() DiscoveryClient discoveryClient = new KubernetesDiscoveryClient( - mockClient, properties, new DefaultIsServicePortSecureResolver(properties)) + mockClient, properties, {client -> client.services()}, new DefaultIsServicePortSecureResolver(properties)) when: List instances = discoveryClient.getInstances("endpoint") @@ -86,7 +89,7 @@ class KubernetesDiscoveryClientTest extends Specification { - def "Should be able to handle endpoints multiple addresses"() { + def "getInstances should be able to handle endpoints multiple addresses"() { given: mockServer.expect().get().withPath("/api/v1/namespaces/test/endpoints/endpoint").andReturn(200, new EndpointsBuilder() .withNewMetadata() @@ -115,7 +118,7 @@ class KubernetesDiscoveryClientTest extends Specification { final properties = new KubernetesDiscoveryProperties() DiscoveryClient discoveryClient = new KubernetesDiscoveryClient( - mockClient, properties, new DefaultIsServicePortSecureResolver(properties)) + mockClient, properties, {client -> client.services()}, new DefaultIsServicePortSecureResolver(properties)) when: List instances = discoveryClient.getInstances("endpoint") @@ -127,4 +130,78 @@ class KubernetesDiscoveryClientTest extends Specification { instances.find({s -> s.host == "ip2" && s.secure}) } + + def "getServices should return all services when no labels are applied to the client"() { + given: + mockServer.expect().get().withPath("/api/v1/namespaces/test/services").andReturn(200, new ServiceListBuilder() + .addNewItem() + .withNewMetadata() + .withName("s1") + .withLabels(new HashMap() {{ + put("label", "value") + }}) + .endMetadata() + .endItem() + .addNewItem() + .withNewMetadata() + .withName("s2") + .withLabels(new HashMap() {{ + put("label", "value") + put("label2", "value2") + }}) + .endMetadata() + .endItem() + .addNewItem() + .withNewMetadata() + .withName("s3") + .endMetadata() + .endItem() + .build()).once() + + and: + DiscoveryClient discoveryClient = new KubernetesDiscoveryClient( + mockClient, new KubernetesDiscoveryProperties(), {client -> client.services()}) + + when: + List instances = discoveryClient.getServices() + + then: + assertThat(instances).containsOnly("s1", "s2", "s3") + } + + def "getServices should return only matching services when labels are applied to the client"() { + given: + // this is the URL that is created by the KubernetesClient when a a label named 'label' + // with a value of 'value' is specified + mockServer.expect().get().withPath("/api/v1/namespaces/test/services?labelSelector=label%3Dvalue").andReturn(200, new ServiceListBuilder() + .addNewItem() + .withNewMetadata() + .withName("s1") + .withLabels(new HashMap() {{ + put("label", "value") + }}) + .endMetadata() + .endItem() + .addNewItem() + .withNewMetadata() + .withName("s2") + .withLabels(new HashMap() {{ + put("label", "value") + put("label2", "value2") + }}) + .endMetadata() + .endItem() + .build()).once() + + and: + DiscoveryClient discoveryClient = new KubernetesDiscoveryClient( + mockClient, + new KubernetesDiscoveryProperties(), {client -> client.services().withLabels(["label": "value"])}) + + when: + List instances = discoveryClient.getServices() + + then: + assertThat(instances).containsOnly("s1", "s2") + } } diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientFilterTest.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientFilterTest.java index 0b1126bb..2936a88b 100644 --- a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientFilterTest.java +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientFilterTest.java @@ -27,6 +27,7 @@ import io.fabric8.kubernetes.api.model.ServiceList; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.ServiceResource; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; @@ -34,6 +35,8 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) @@ -45,12 +48,18 @@ public class KubernetesDiscoveryClientFilterTest { @Mock private KubernetesDiscoveryProperties properties; + private KubernetesClientServicesFunction kubernetesClientServicesFunction = KubernetesClient::services; + @Mock private MixedOperation> serviceOperation; - @InjectMocks private KubernetesDiscoveryClient underTest; + @Before + public void setUp() { + underTest = new KubernetesDiscoveryClient(kubernetesClient, properties, kubernetesClientServicesFunction); + } + @Test public void testFilteredServices() { List springBootServiceNames = Arrays.asList("serviceA", "serviceB");