Make the source of Service Metadata configurable (#270)

Make service metadata configurable, so that users can tune it
however they see fit. Specifically we add:

* Add flag to enable/disable addition of labels to metadata
(default: true)
* Add optional prefix to label keys (default none)
* Add flag to enable/disable addition of annotation to metadata
(default: true)
* Add optional prefix to annotation keys (default none)

Fixes: gh-260
This commit is contained in:
Georgios Andrianakis
2018-11-15 22:27:52 +02:00
committed by Spencer Gibb
parent 0b8e1c9810
commit b39997a820
4 changed files with 333 additions and 11 deletions

View File

@@ -23,6 +23,7 @@ import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.client.KubernetesClient;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
@@ -35,6 +36,7 @@ import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
public class KubernetesDiscoveryClient implements DiscoveryClient {
@@ -72,19 +74,35 @@ public class KubernetesDiscoveryClient implements DiscoveryClient {
public List<ServiceInstance> getInstances(String serviceId) {
Assert.notNull(serviceId,
"[Assertion failed] - the object argument must be null");
final Map<String, String> labels = getLabels(serviceId);
Endpoints endpoints = client.endpoints().withName(serviceId).get();
List<EndpointSubset> subsets = null != endpoints ? endpoints.getSubsets() : new ArrayList<>();
List<ServiceInstance> instances = new ArrayList<>();
if (!subsets.isEmpty()) {
final Service service = client.services().withName(serviceId).get();
final Map<String, String> metadata = new HashMap<>();
if(properties.isEnabledAdditionOfLabelsAsMetadata()) {
metadata.putAll(
getMapWithPrefixedKeys(
service.getMetadata().getLabels(), properties.getLabelKeysPrefix())
);
}
if(properties.isEnabledAdditionOfAnnotationsAsMetadata()) {
metadata.putAll(
getMapWithPrefixedKeys(
service.getMetadata().getAnnotations(), properties.getAnnotationKeysPrefix())
);
}
for (EndpointSubset s : subsets) {
List<EndpointAddress> addresses = s.getAddresses();
for (EndpointAddress a : addresses) {
instances.add(new KubernetesServiceInstance(serviceId,
a,
s.getPorts().stream().findFirst().orElseThrow(IllegalStateException::new),
labels,
metadata,
false));
}
}
@@ -93,12 +111,19 @@ public class KubernetesDiscoveryClient implements DiscoveryClient {
return instances;
}
private Map<String, String> getLabels(String serviceName) {
final Service service = client.services().withName(serviceName).get();
if (service != null) {
return service.getMetadata().getLabels();
// returns a new map that contain all the entries of the original map
// but with the keys prefixed
// if the prefix is null or empty, the map itself is returned (unchanged of course)
private Map<String, String> getMapWithPrefixedKeys(Map<String, String> map, String prefix) {
// when the prefix is empty just return an map with the same entries
if (!StringUtils.hasText(prefix)) {
return map;
}
return Collections.emptyMap();
final Map<String, String> result = new HashMap<>();
map.forEach((k, v) -> result.put(prefix + k, v));
return result;
}
@Override

View File

@@ -29,11 +29,36 @@ public class KubernetesDiscoveryProperties extends AutoServiceRegistrationProper
@Value("${spring.application.name:unknown}")
private String serviceName = "unknown";
/**
* SpEL expression to filter services
/**
* SpEL expression to filter services
**/
private String filter;
/**
* When set, the Kubernetes labels of the services will be included as metadata
* of the returned ServiceInstance
*/
private boolean enabledAdditionOfLabelsAsMetadata = true;
/**
* When enabledAdditionOfLabelsAsMetadata is set, then the value labelKeysPrefix
* will be used as a prefix to the key names in the metadata map
*/
private String labelKeysPrefix;
/**
* When set, the Kubernetes annotations of the services will be included as metadata
* of the returned ServiceInstance
*/
private boolean enabledAdditionOfAnnotationsAsMetadata = true;
/**
* When enabledAdditionOfAnnotationsAsMetadata is set, then the value annotationKeysPrefix
* will be used as a prefix to the key names in the metadata map
*/
private String annotationKeysPrefix;
public boolean isEnabled() {
return enabled;
}
@@ -49,11 +74,44 @@ public class KubernetesDiscoveryProperties extends AutoServiceRegistrationProper
public String getFilter() {
return filter;
}
public void setFilter(String filter){
this.filter = filter;
}
public boolean isEnabledAdditionOfLabelsAsMetadata() {
return enabledAdditionOfLabelsAsMetadata;
}
public void setEnabledAdditionOfLabelsAsMetadata(boolean enabledAdditionOfLabelsAsMetadata) {
this.enabledAdditionOfLabelsAsMetadata = enabledAdditionOfLabelsAsMetadata;
}
public String getLabelKeysPrefix() {
return labelKeysPrefix;
}
public void setLabelKeysPrefix(String labelKeysPrefix) {
this.labelKeysPrefix = labelKeysPrefix;
}
public boolean isEnabledAdditionOfAnnotationsAsMetadata() {
return enabledAdditionOfAnnotationsAsMetadata;
}
public void setEnabledAdditionOfAnnotationsAsMetadata(
boolean enabledAdditionOfAnnotationsAsMetadata) {
this.enabledAdditionOfAnnotationsAsMetadata = enabledAdditionOfAnnotationsAsMetadata;
}
public String getAnnotationKeysPrefix() {
return annotationKeysPrefix;
}
public void setAnnotationKeysPrefix(String annotationKeysPrefix) {
this.annotationKeysPrefix = annotationKeysPrefix;
}
@Override
public String toString() {
return "KubernetesDiscoveryProperties{" +

View File

@@ -0,0 +1,239 @@
/*
* 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 static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.Mockito.when;
import io.fabric8.kubernetes.api.model.DoneableEndpoints;
import io.fabric8.kubernetes.api.model.DoneableService;
import io.fabric8.kubernetes.api.model.Endpoints;
import io.fabric8.kubernetes.api.model.EndpointsBuilder;
import io.fabric8.kubernetes.api.model.EndpointsList;
import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.api.model.ServiceBuilder;
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.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.cloud.client.ServiceInstance;
@RunWith(MockitoJUnitRunner.class)
public class KubernetesDiscoveryClientFilterMetadataTest {
@Mock
private KubernetesClient kubernetesClient;
@Mock
private KubernetesDiscoveryProperties properties;
@Mock
private MixedOperation<Service, ServiceList, DoneableService, Resource<Service, DoneableService>> serviceOperation;
@Mock
private MixedOperation<Endpoints, EndpointsList, DoneableEndpoints, Resource<Endpoints, DoneableEndpoints>> endpointsOperation;
@Mock
private Resource<Service, DoneableService> serviceResource;
@Mock
private Resource<Endpoints, DoneableEndpoints> endpointsResource;
@InjectMocks
private KubernetesDiscoveryClient underTest;
@Test
public void testBothLabelsAndAnnotationsDisabled() {
final String serviceId = "s";
when(properties.isEnabledAdditionOfLabelsAsMetadata()).thenReturn(false);
when(properties.isEnabledAdditionOfAnnotationsAsMetadata()).thenReturn(false);
setupServiceWithLabelsAndAnnotations(
serviceId,
new HashMap<String, String>() {{
put("l1", "lab");
}},
new HashMap<String, String>() {{
put("l1", "lab");
}}
);
final List<ServiceInstance> instances = underTest.getInstances(serviceId);
assertThat(instances).hasSize(1);
assertThat(instances.get(0).getMetadata()).isEmpty();
}
@Test
public void testLabelsEnabledAndAnnotationsDisabled() {
final String serviceId = "s";
when(properties.isEnabledAdditionOfLabelsAsMetadata()).thenReturn(true);
when(properties.isEnabledAdditionOfAnnotationsAsMetadata()).thenReturn(false);
setupServiceWithLabelsAndAnnotations(
serviceId,
new HashMap<String, String>() {{
put("l1", "v1");
put("l2", "v2");
}},
new HashMap<String, String>() {{
put("l1", "lab");
}}
);
final List<ServiceInstance> instances = underTest.getInstances(serviceId);
assertThat(instances).hasSize(1);
assertThat(instances.get(0).getMetadata()).containsOnly(entry("l1", "v1"), entry("l2", "v2"));
}
@Test
public void testLabelsEnabledAndAnnotationsDisabledWithPrefix() {
final String serviceId = "s";
when(properties.isEnabledAdditionOfLabelsAsMetadata()).thenReturn(true);
when(properties.getLabelKeysPrefix()).thenReturn("l_");
when(properties.isEnabledAdditionOfAnnotationsAsMetadata()).thenReturn(false);
setupServiceWithLabelsAndAnnotations(
serviceId,
new HashMap<String, String>() {{
put("l1", "v1");
put("l2", "v2");
}},
new HashMap<String, String>() {{
put("l1", "lab");
}}
);
final List<ServiceInstance> instances = underTest.getInstances(serviceId);
assertThat(instances).hasSize(1);
assertThat(instances.get(0).getMetadata()).containsOnly(entry("l_l1", "v1"), entry("l_l2", "v2"));
}
@Test
public void testLabelsDisabledAndAnnotationsEnabled() {
final String serviceId = "s";
when(properties.isEnabledAdditionOfLabelsAsMetadata()).thenReturn(false);
when(properties.isEnabledAdditionOfAnnotationsAsMetadata()).thenReturn(true);
setupServiceWithLabelsAndAnnotations(
serviceId,
new HashMap<String, String>() {{
put("l1", "v1");
}},
new HashMap<String, String>() {{
put("a1", "v1");
put("a2", "v2");
}}
);
final List<ServiceInstance> instances = underTest.getInstances(serviceId);
assertThat(instances).hasSize(1);
assertThat(instances.get(0).getMetadata()).containsOnly(entry("a1", "v1"), entry("a2", "v2"));
}
@Test
public void testLabelsDisabledAndAnnotationsEnabledWithPrefix() {
final String serviceId = "s";
when(properties.isEnabledAdditionOfLabelsAsMetadata()).thenReturn(false);
when(properties.isEnabledAdditionOfAnnotationsAsMetadata()).thenReturn(true);
when(properties.getAnnotationKeysPrefix()).thenReturn("a_");
setupServiceWithLabelsAndAnnotations(
serviceId,
new HashMap<String, String>() {{
put("l1", "v1");
}},
new HashMap<String, String>() {{
put("a1", "v1");
put("a2", "v2");
}}
);
final List<ServiceInstance> instances = underTest.getInstances(serviceId);
assertThat(instances).hasSize(1);
assertThat(instances.get(0).getMetadata()).containsOnly(entry("a_a1", "v1"), entry("a_a2", "v2"));
}
@Test
public void testBothLabelsAndAnnotationsEnabledWithPrefix() {
final String serviceId = "s";
when(properties.isEnabledAdditionOfLabelsAsMetadata()).thenReturn(true);
when(properties.getLabelKeysPrefix()).thenReturn("l_");
when(properties.isEnabledAdditionOfAnnotationsAsMetadata()).thenReturn(true);
when(properties.getAnnotationKeysPrefix()).thenReturn("a_");
setupServiceWithLabelsAndAnnotations(
serviceId,
new HashMap<String, String>() {{
put("l1", "la1");
}},
new HashMap<String, String>() {{
put("a1", "an1");
put("a2", "an2");
}}
);
final List<ServiceInstance> instances = underTest.getInstances(serviceId);
assertThat(instances).hasSize(1);
assertThat(instances.get(0).getMetadata()).containsOnly(
entry("a_a1", "an1"), entry("a_a2", "an2"), entry("l_l1", "la1"));
}
private void setupServiceWithLabelsAndAnnotations(String serviceId,
Map<String, String> labels, Map<String, String> annotations) {
final Service service =
new ServiceBuilder()
.withNewMetadata()
.withLabels(labels)
.withAnnotations(annotations)
.endMetadata()
.build();
when(serviceOperation.withName(serviceId)).thenReturn(serviceResource);
when(serviceResource.get()).thenReturn(service);
when(kubernetesClient.services()).thenReturn(serviceOperation);
final Endpoints endpoints =
new EndpointsBuilder()
.addNewSubset()
.addNewPort()
.endPort()
.addNewAddress()
.endAddress()
.endSubset()
.build();
when(endpointsResource.get()).thenReturn(endpoints);
when(endpointsOperation.withName(serviceId)).thenReturn(endpointsResource);
when(kubernetesClient.endpoints()).thenReturn(endpointsOperation);
}
}

View File

@@ -37,7 +37,7 @@ import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class KubernetesDiscoveryClientTest {
public class KubernetesDiscoveryClientFilterTest {
@Mock
private KubernetesClient kubernetesClient;