From 8e9227b13fefd6488768cd6dc7df8e1002d4c1f0 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Tue, 15 Jan 2019 11:45:42 +0200 Subject: [PATCH 1/3] Add the ability to only discover services that match certain labels Arbitrary filtering of services is also possible by supplying a bean of type KubernetesClientServicesFunction --- .../KubernetesClientServicesFunction.java | 29 ++++++ .../discovery/KubernetesDiscoveryClient.java | 28 +++--- ...netesDiscoveryClientAutoConfiguration.java | 15 ++- .../KubernetesDiscoveryProperties.java | 16 ++- .../KubernetesDiscoveryClientTest.groovy | 99 +++++++++++++++++-- .../KubernetesDiscoveryClientFilterTest.java | 11 ++- 6 files changed, 171 insertions(+), 27 deletions(-) create mode 100644 spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesClientServicesFunction.java 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..b358ee87 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesClientServicesFunction.java @@ -0,0 +1,29 @@ +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 67f0b389..05233748 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,22 +16,10 @@ */ 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; -import io.fabric8.kubernetes.api.model.Endpoints; -import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.*; 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 +28,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 { @@ -48,6 +43,7 @@ public class KubernetesDiscoveryClient implements DiscoveryClient { private KubernetesClient client; private final KubernetesDiscoveryProperties properties; + private final KubernetesClientServicesFunction kubernetesClientServicesFunction; private final SpelExpressionParser parser = new SpelExpressionParser(); private final SimpleEvaluationContext evalCtxt = SimpleEvaluationContext .forReadOnlyDataBinding() @@ -55,10 +51,12 @@ public class KubernetesDiscoveryClient implements DiscoveryClient { .build(); public KubernetesDiscoveryClient(KubernetesClient client, - KubernetesDiscoveryProperties kubernetesDiscoveryProperties) { + KubernetesDiscoveryProperties kubernetesDiscoveryProperties, + KubernetesClientServicesFunction kubernetesClientServicesFunction) { this.client = client; this.properties = kubernetesDiscoveryProperties; + this.kubernetesClientServicesFunction = kubernetesClientServicesFunction; } public KubernetesClient getClient() { @@ -172,7 +170,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 3b444e27..c92e1ed7 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 @@ -18,7 +18,6 @@ package org.springframework.cloud.kubernetes.discovery; import io.fabric8.kubernetes.client.KubernetesClient; - import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -35,12 +34,22 @@ import org.springframework.context.annotation.Configuration; CommonsClientAutoConfiguration.class, }) public class KubernetesDiscoveryClientAutoConfiguration { + @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) { - return new KubernetesDiscoveryClient(client, properties); + KubernetesDiscoveryProperties properties, + KubernetesClientServicesFunction kubernetesClientServicesFunction) { + return new KubernetesDiscoveryClient(client, properties, kubernetesClientServicesFunction); } @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 bf99106f..21c3a0d2 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,6 +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.HashMap; +import java.util.Map; + @ConfigurationProperties("spring.cloud.kubernetes.discovery") public class KubernetesDiscoveryProperties { @@ -31,9 +34,12 @@ 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; + /** 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() { @@ -60,6 +66,14 @@ public class KubernetesDiscoveryProperties { this.filter = filter; } + public Map getServiceLabels() { + return serviceLabels; + } + + public void setServiceLabels(Map serviceLabels) { + this.serviceLabels = serviceLabels; + } + public Metadata getMetadata() { return metadata; } 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 923c1f9f..8755b075 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,13 +19,17 @@ 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 +import org.assertj.core.api.Assertions import org.springframework.cloud.client.ServiceInstance import org.springframework.cloud.client.discovery.DiscoveryClient import spock.lang.Specification +import static org.assertj.core.api.Assertions.* + class KubernetesDiscoveryClientTest extends Specification { private static KubernetesMockServer mockServer = new KubernetesMockServer() @@ -47,7 +51,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() @@ -70,10 +74,14 @@ class KubernetesDiscoveryClientTest extends Specification { .endMetadata() .build()).once() - DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, new KubernetesDiscoveryProperties()) - when: + and: + DiscoveryClient discoveryClient = new KubernetesDiscoveryClient( + mockClient, new KubernetesDiscoveryProperties(), {client -> client.services()}) + + when: List instances = discoveryClient.getInstances("endpoint") - then: + + then: instances != null instances.size() == 1 instances.find({s -> s.host == "ip1"}) @@ -81,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() @@ -108,8 +116,11 @@ class KubernetesDiscoveryClientTest extends Specification { .endMetadata() .build()).once() - DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, new KubernetesDiscoveryProperties()) - when: + and: + DiscoveryClient discoveryClient = new KubernetesDiscoveryClient( + mockClient, new KubernetesDiscoveryProperties(), {client -> client.services()}) + + when: List instances = discoveryClient.getInstances("endpoint") then: instances != null @@ -118,4 +129,78 @@ class KubernetesDiscoveryClientTest extends Specification { instances.find({s -> s.host == "ip2"}) } + + 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"); From 2a1baa69a8b90935879e289c8ea8413d97689230 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Tue, 15 Jan 2019 18:05:50 +0200 Subject: [PATCH 2/3] Address review concerns --- .../KubernetesClientServicesFunction.java | 16 ++++++++++++++++ .../discovery/KubernetesDiscoveryClient.java | 6 +++++- .../KubernetesDiscoveryClientTest.groovy | 3 +-- 3 files changed, 22 insertions(+), 3 deletions(-) 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 index b358ee87..daa6b836 100644 --- 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 @@ -1,3 +1,19 @@ +/* + * 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; 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 05233748..034ac9c2 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,7 +16,11 @@ */ package org.springframework.cloud.kubernetes.discovery; -import io.fabric8.kubernetes.api.model.*; +import io.fabric8.kubernetes.api.model.EndpointAddress; +import io.fabric8.kubernetes.api.model.EndpointPort; +import io.fabric8.kubernetes.api.model.EndpointSubset; +import io.fabric8.kubernetes.api.model.Endpoints; +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; 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 8755b075..350d6eab 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 @@ -23,12 +23,11 @@ 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 -import org.assertj.core.api.Assertions import org.springframework.cloud.client.ServiceInstance import org.springframework.cloud.client.discovery.DiscoveryClient import spock.lang.Specification -import static org.assertj.core.api.Assertions.* +import static org.assertj.core.api.Assertions.assertThat class KubernetesDiscoveryClientTest extends Specification { From 740c4fd648f12ca890e12ee178b51003c7d8b3a3 Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Wed, 16 Jan 2019 17:46:14 -0500 Subject: [PATCH 3/3] fixes merge misses --- .../discovery/KubernetesDiscoveryClientTest.groovy | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 edbe3634..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 @@ -50,7 +50,7 @@ class KubernetesDiscoveryClientTest extends Specification { mockServer.destroy() } - def "getInstances 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() @@ -76,7 +76,7 @@ class KubernetesDiscoveryClientTest extends Specification { final properties = new KubernetesDiscoveryProperties() DiscoveryClient discoveryClient = new KubernetesDiscoveryClient( - mockClient, properties, {client -> client.services(), new DefaultIsServicePortSecureResolver(properties)) + mockClient, properties, {client -> client.services()}, new DefaultIsServicePortSecureResolver(properties)) when: List instances = discoveryClient.getInstances("endpoint") @@ -89,7 +89,7 @@ class KubernetesDiscoveryClientTest extends Specification { - def "getInstances 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() @@ -118,7 +118,7 @@ class KubernetesDiscoveryClientTest extends Specification { final properties = new KubernetesDiscoveryProperties() DiscoveryClient discoveryClient = new KubernetesDiscoveryClient( - mockClient, properties, {client -> client.services(), new DefaultIsServicePortSecureResolver(properties)) + mockClient, properties, {client -> client.services()}, new DefaultIsServicePortSecureResolver(properties)) when: List instances = discoveryClient.getInstances("endpoint") @@ -129,7 +129,7 @@ class KubernetesDiscoveryClientTest extends Specification { instances.find({s -> s.host == "ip1" && s.secure}) instances.find({s -> s.host == "ip2" && s.secure}) - } + } def "getServices should return all services when no labels are applied to the client"() { given: @@ -142,7 +142,7 @@ class KubernetesDiscoveryClientTest extends Specification { }}) .endMetadata() .endItem() - .addNewItem() + .addNewItem() .withNewMetadata() .withName("s2") .withLabels(new HashMap() {{ @@ -150,7 +150,7 @@ class KubernetesDiscoveryClientTest extends Specification { put("label2", "value2") }}) .endMetadata() - .endItem() + .endItem() .addNewItem() .withNewMetadata() .withName("s3")