Merge branch 'geoand-284'

This commit is contained in:
Spencer Gibb
2019-01-16 17:47:27 -05:00
6 changed files with 183 additions and 25 deletions

View File

@@ -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<KubernetesClient, FilterWatchListDeletable<Service, ServiceList, Boolean, Watch, Watcher<Service>>> {
}

View File

@@ -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<String> getServices(Predicate<Service> filter) {
return client.services().list()
return kubernetesClientServicesFunction.apply(client).list()
.getItems()
.stream()
.filter(filter)

View File

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

View File

@@ -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<String, String> serviceLabels = new HashMap<>();
private Metadata metadata = new Metadata();
public boolean isEnabled() {
@@ -80,6 +83,14 @@ public class KubernetesDiscoveryProperties {
this.knownSecurePorts = knownSecurePorts;
}
public Map<String, String> getServiceLabels() {
return serviceLabels;
}
public void setServiceLabels(Map<String, String> 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();
}

View File

@@ -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<ServiceInstance> 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<ServiceInstance> 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<String, String>() {{
put("label", "value")
}})
.endMetadata()
.endItem()
.addNewItem()
.withNewMetadata()
.withName("s2")
.withLabels(new HashMap<String, String>() {{
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<String> 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<String, String>() {{
put("label", "value")
}})
.endMetadata()
.endItem()
.addNewItem()
.withNewMetadata()
.withName("s2")
.withLabels(new HashMap<String, String>() {{
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<String> instances = discoveryClient.getServices()
then:
assertThat(instances).containsOnly("s1", "s2")
}
}

View File

@@ -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<Service, ServiceList, DoneableService, ServiceResource<Service, DoneableService>> serviceOperation;
@InjectMocks
private KubernetesDiscoveryClient underTest;
@Before
public void setUp() {
underTest = new KubernetesDiscoveryClient(kubernetesClient, properties, kubernetesClientServicesFunction);
}
@Test
public void testFilteredServices() {
List<String> springBootServiceNames = Arrays.asList("serviceA", "serviceB");