From b39997a820476148fe3cc98b465e8ceaeb131e9f Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Thu, 15 Nov 2018 22:27:52 +0200 Subject: [PATCH] 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 --- .../discovery/KubernetesDiscoveryClient.java | 39 ++- .../KubernetesDiscoveryProperties.java | 64 ++++- ...etesDiscoveryClientFilterMetadataTest.java | 239 ++++++++++++++++++ ... KubernetesDiscoveryClientFilterTest.java} | 2 +- 4 files changed, 333 insertions(+), 11 deletions(-) create mode 100644 spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientFilterMetadataTest.java rename spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/{KubernetesDiscoveryClientTest.java => KubernetesDiscoveryClientFilterTest.java} (98%) 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 589a460f..abe5fd96 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 @@ -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 getInstances(String serviceId) { Assert.notNull(serviceId, "[Assertion failed] - the object argument must be null"); - final Map labels = getLabels(serviceId); Endpoints endpoints = client.endpoints().withName(serviceId).get(); List subsets = null != endpoints ? endpoints.getSubsets() : new ArrayList<>(); List instances = new ArrayList<>(); if (!subsets.isEmpty()) { + + final Service service = client.services().withName(serviceId).get(); + + final Map 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 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 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 getMapWithPrefixedKeys(Map 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 result = new HashMap<>(); + map.forEach((k, v) -> result.put(prefix + k, v)); + + return result; } @Override 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 5c80ea1f..07206816 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 @@ -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{" + diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientFilterMetadataTest.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientFilterMetadataTest.java new file mode 100644 index 00000000..eefdd44c --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientFilterMetadataTest.java @@ -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> serviceOperation; + + @Mock + private MixedOperation> endpointsOperation; + + @Mock + private Resource serviceResource; + + @Mock + private Resource 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() {{ + put("l1", "lab"); + }}, + new HashMap() {{ + put("l1", "lab"); + }} + ); + + final List 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() {{ + put("l1", "v1"); + put("l2", "v2"); + }}, + new HashMap() {{ + put("l1", "lab"); + }} + ); + + final List 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() {{ + put("l1", "v1"); + put("l2", "v2"); + }}, + new HashMap() {{ + put("l1", "lab"); + }} + ); + + final List 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() {{ + put("l1", "v1"); + }}, + new HashMap() {{ + put("a1", "v1"); + put("a2", "v2"); + }} + ); + + final List 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() {{ + put("l1", "v1"); + }}, + new HashMap() {{ + put("a1", "v1"); + put("a2", "v2"); + }} + ); + + final List 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() {{ + put("l1", "la1"); + }}, + new HashMap() {{ + put("a1", "an1"); + put("a2", "an2"); + }} + ); + + final List 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 labels, Map 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); + } + + +} diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientTest.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientFilterTest.java similarity index 98% rename from spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientTest.java rename to spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientFilterTest.java index c5fce284..9b46de19 100644 --- a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientTest.java +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientFilterTest.java @@ -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;