diff --git a/pom.xml b/pom.xml
index d8402324..749c19be 100644
--- a/pom.xml
+++ b/pom.xml
@@ -106,6 +106,7 @@
docs
spring-cloud-kubernetes-loadbalancer
spring-cloud-starter-kubernetes-loadbalancer
+ spring-cloud-kubernetes-client-discovery
diff --git a/spring-cloud-kubernetes-client-autoconfig/pom.xml b/spring-cloud-kubernetes-client-autoconfig/pom.xml
index 0a60ebab..84c54be0 100644
--- a/spring-cloud-kubernetes-client-autoconfig/pom.xml
+++ b/spring-cloud-kubernetes-client-autoconfig/pom.xml
@@ -24,6 +24,10 @@
io.kubernetes
client-java-extended
+
+ io.kubernetes
+ client-java-spring-integration
+
org.springframework.boot
spring-boot-actuator-autoconfigure
diff --git a/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientAutoConfiguration.java b/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientAutoConfiguration.java
index 732bf4b6..d4c04a0d 100644
--- a/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientAutoConfiguration.java
+++ b/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientAutoConfiguration.java
@@ -46,10 +46,16 @@ public class KubernetesClientAutoConfiguration {
@Bean
@ConditionalOnMissingBean
- public CoreV1Api coreApi() throws IOException {
+ public ApiClient apiClient() throws IOException {
ApiClient apiClient = kubernetesApiClient();
io.kubernetes.client.openapi.Configuration.setDefaultApiClient(apiClient);
- return new CoreV1Api();
+ return apiClient;
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public CoreV1Api coreApi(ApiClient apiClient) throws IOException {
+ return new CoreV1Api(apiClient);
}
@Bean
diff --git a/spring-cloud-kubernetes-client-discovery/pom.xml b/spring-cloud-kubernetes-client-discovery/pom.xml
new file mode 100644
index 00000000..8123cb5b
--- /dev/null
+++ b/spring-cloud-kubernetes-client-discovery/pom.xml
@@ -0,0 +1,61 @@
+
+
+
+ spring-cloud-kubernetes
+ org.springframework.cloud
+ 2.0.0-SNAPSHOT
+
+ 4.0.0
+
+ spring-cloud-kubernetes-client-discovery
+ Spring Cloud Kubernetes :: Kubernetes Client Discovery
+
+
+
+ org.springframework.cloud
+ spring-cloud-kubernetes-client-autoconfig
+ ${project.version}
+
+
+ org.springframework.cloud
+ spring-cloud-commons
+ ${spring-cloud-commons.version}
+
+
+ org.springframework.boot
+ spring-boot-actuator
+ true
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.junit.vintage
+ junit-vintage-engine
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+ test
+
+
+ org.springframework.cloud
+ spring-cloud-config-client
+ test
+
+
+
+
+
diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/ConditionalOnKubernetesDiscoveryEnabled.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/ConditionalOnKubernetesDiscoveryEnabled.java
new file mode 100644
index 00000000..bb0f9a07
--- /dev/null
+++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/ConditionalOnKubernetesDiscoveryEnabled.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2019-2019 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
+ *
+ * https://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.client.discovery;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+@ConditionalOnProperty(value = "spring.cloud.kubernetes.discovery.enabled", matchIfMissing = true)
+public @interface ConditionalOnKubernetesDiscoveryEnabled {
+
+}
diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientConfigClientBootstrapConfiguration.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientConfigClientBootstrapConfiguration.java
new file mode 100644
index 00000000..15eb1e51
--- /dev/null
+++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientConfigClientBootstrapConfiguration.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2013-2019 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
+ *
+ * https://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.client.discovery;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnProperty("spring.cloud.config.discovery.enabled")
+@Import({ KubernetesClientAutoConfiguration.class, KubernetesReactiveDiscoveryClientAutoConfiguration.class })
+public class KubernetesDiscoveryClientConfigClientBootstrapConfiguration {
+
+}
diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryProperties.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryProperties.java
new file mode 100644
index 00000000..a20863fc
--- /dev/null
+++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryProperties.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright 2013-2019 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
+ *
+ * https://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.client.discovery;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.cloud.client.discovery.DiscoveryClient;
+import org.springframework.core.style.ToStringCreator;
+
+@ConfigurationProperties("spring.cloud.kubernetes.discovery")
+public class KubernetesDiscoveryProperties {
+
+ /** If Kubernetes Discovery is enabled. */
+ private boolean enabled = true;
+
+ /** The service name of the local instance. */
+ @Value("${spring.application.name:unknown}")
+ private String serviceName = "unknown";
+
+ /** If discovering all namespaces. */
+ private boolean allNamespaces = false;
+
+ /*
+ * If wait for the discovery cache (service and endpoints) to be fully loaded,
+ * otherwise aborts the application on starting.
+ */
+ private boolean waitCacheReady = true;
+
+ /** Timeout for initializing discovery cache, will abort the application if exceeded. **/
+ private long cacheLoadingTimeoutSeconds = 60;
+
+ /**
+ * SpEL expression to filter services AFTER they have been retrieved from the
+ * Kubernetes API server.
+ */
+ private String filter;
+
+ /** Set the port numbers that are considered secure and use HTTPS. */
+ private Set knownSecurePorts = new HashSet() {
+ {
+ add(443);
+ add(8443);
+ }
+ };
+
+ /**
+ * If set, then only the services matching these labels will be fetched from the
+ * Kubernetes API server.
+ */
+ private Map serviceLabels = new HashMap<>();
+
+ /**
+ * If set then the port with a given name is used as primary when multiple ports are
+ * defined for a service.
+ */
+ private String primaryPortName;
+
+ private Metadata metadata = new Metadata();
+
+ private int order = DiscoveryClient.DEFAULT_ORDER;
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public String getServiceName() {
+ return this.serviceName;
+ }
+
+ public void setServiceName(String serviceName) {
+ this.serviceName = serviceName;
+ }
+
+ public String getFilter() {
+ return this.filter;
+ }
+
+ public void setFilter(String filter) {
+ this.filter = filter;
+ }
+
+ public Set getKnownSecurePorts() {
+ return this.knownSecurePorts;
+ }
+
+ public void setKnownSecurePorts(Set knownSecurePorts) {
+ this.knownSecurePorts = knownSecurePorts;
+ }
+
+ public Map getServiceLabels() {
+ return this.serviceLabels;
+ }
+
+ public void setServiceLabels(Map serviceLabels) {
+ this.serviceLabels = serviceLabels;
+ }
+
+ public String getPrimaryPortName() {
+ return primaryPortName;
+ }
+
+ public void setPrimaryPortName(String primaryPortName) {
+ this.primaryPortName = primaryPortName;
+ }
+
+ public Metadata getMetadata() {
+ return this.metadata;
+ }
+
+ public void setMetadata(Metadata metadata) {
+ this.metadata = metadata;
+ }
+
+ public boolean isAllNamespaces() {
+ return allNamespaces;
+ }
+
+ public void setAllNamespaces(boolean allNamespaces) {
+ this.allNamespaces = allNamespaces;
+ }
+
+ public int getOrder() {
+ return this.order;
+ }
+
+ public void setOrder(int order) {
+ this.order = order;
+ }
+
+ boolean isWaitCacheReady() {
+ return waitCacheReady;
+ }
+
+ void setWaitCacheReady(boolean waitCacheReady) {
+ this.waitCacheReady = waitCacheReady;
+ }
+
+ long getCacheLoadingTimeoutSeconds() {
+ return cacheLoadingTimeoutSeconds;
+ }
+
+ void setCacheLoadingTimeoutSeconds(long cacheLoadingTimeoutSeconds) {
+ this.cacheLoadingTimeoutSeconds = cacheLoadingTimeoutSeconds;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringCreator(this).append("enabled", this.enabled).append("serviceName", this.serviceName)
+ .append("filter", this.filter).append("knownSecurePorts", this.knownSecurePorts)
+ .append("serviceLabels", this.serviceLabels).append("metadata", this.metadata).toString();
+ }
+
+ /**
+ * Metadata properties.
+ */
+ public class Metadata {
+
+ /**
+ * When set, the Kubernetes labels of the services will be included as metadata of
+ * the returned ServiceInstance.
+ */
+ private boolean addLabels = true;
+
+ /**
+ * When addLabels is set, then this will be used as a prefix to the key names in
+ * the metadata map.
+ */
+ private String labelsPrefix;
+
+ /**
+ * When set, the Kubernetes annotations of the services will be included as
+ * metadata of the returned ServiceInstance.
+ */
+ private boolean addAnnotations = true;
+
+ /**
+ * When addAnnotations is set, then this will be used as a prefix to the key names
+ * in the metadata map.
+ */
+ private String annotationsPrefix;
+
+ /**
+ * When set, any named Kubernetes service ports will be included as metadata of
+ * the returned ServiceInstance.
+ */
+ private boolean addPorts = true;
+
+ /**
+ * When addPorts is set, then this will be used as a prefix to the key names in
+ * the metadata map.
+ */
+ private String portsPrefix = "port.";
+
+ public boolean isAddLabels() {
+ return this.addLabels;
+ }
+
+ public void setAddLabels(boolean addLabels) {
+ this.addLabels = addLabels;
+ }
+
+ public String getLabelsPrefix() {
+ return this.labelsPrefix;
+ }
+
+ public void setLabelsPrefix(String labelsPrefix) {
+ this.labelsPrefix = labelsPrefix;
+ }
+
+ public boolean isAddAnnotations() {
+ return this.addAnnotations;
+ }
+
+ public void setAddAnnotations(boolean addAnnotations) {
+ this.addAnnotations = addAnnotations;
+ }
+
+ public String getAnnotationsPrefix() {
+ return this.annotationsPrefix;
+ }
+
+ public void setAnnotationsPrefix(String annotationsPrefix) {
+ this.annotationsPrefix = annotationsPrefix;
+ }
+
+ public boolean isAddPorts() {
+ return this.addPorts;
+ }
+
+ public void setAddPorts(boolean addPorts) {
+ this.addPorts = addPorts;
+ }
+
+ public String getPortsPrefix() {
+ return this.portsPrefix;
+ }
+
+ public void setPortsPrefix(String portsPrefix) {
+ this.portsPrefix = portsPrefix;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringCreator(this).append("addLabels", this.addLabels)
+ .append("labelsPrefix", this.labelsPrefix).append("addAnnotations", this.addAnnotations)
+ .append("annotationsPrefix", this.annotationsPrefix).append("addPorts", this.addPorts)
+ .append("portsPrefix", this.portsPrefix).toString();
+ }
+
+ }
+
+}
diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClient.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClient.java
new file mode 100644
index 00000000..beebede7
--- /dev/null
+++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClient.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2013-2019 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
+ *
+ * https://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.client.discovery;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import io.kubernetes.client.extended.wait.Wait;
+import io.kubernetes.client.informer.SharedInformer;
+import io.kubernetes.client.informer.SharedInformerFactory;
+import io.kubernetes.client.informer.cache.Lister;
+import io.kubernetes.client.openapi.models.V1EndpointPort;
+import io.kubernetes.client.openapi.models.V1Endpoints;
+import io.kubernetes.client.openapi.models.V1Service;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.discovery.DiscoveryClient;
+import org.springframework.util.Assert;
+
+public class KubernetesInformerDiscoveryClient implements DiscoveryClient, InitializingBean {
+
+ private static final Log log = LogFactory.getLog(KubernetesInformerDiscoveryClient.class);
+
+ private final SharedInformerFactory sharedInformerFactory;
+
+ private final Lister serviceLister;
+
+ private final Supplier informersReadyFunc;
+
+ private final Lister endpointsLister;
+
+ private final KubernetesDiscoveryProperties properties;
+
+ private final String namespace;
+
+ public KubernetesInformerDiscoveryClient(String namespace, SharedInformerFactory sharedInformerFactory,
+ Lister serviceLister, Lister endpointsLister,
+ SharedInformer serviceInformer, SharedInformer endpointsInformer,
+ KubernetesDiscoveryProperties properties) {
+ this.namespace = namespace;
+ this.sharedInformerFactory = sharedInformerFactory;
+
+ this.serviceLister = serviceLister;
+ this.endpointsLister = endpointsLister;
+ this.informersReadyFunc = () -> serviceInformer.hasSynced() && endpointsInformer.hasSynced();
+
+ this.properties = properties;
+ }
+
+ @Override
+ public String description() {
+ return "Kubernetes Client Discovery";
+ }
+
+ @Override
+ public List getInstances(String serviceId) {
+ Assert.notNull(serviceId, "[Assertion failed] - the object argument must not be null");
+
+ V1Service service = properties.isAllNamespaces() ? this.serviceLister.list().stream()
+ .filter(svc -> serviceId.equals(svc.getMetadata().getName())).findFirst().orElse(null)
+ : this.serviceLister.namespace(this.namespace).get(serviceId);
+ if (service == null) {
+ // no such service present in the cluster
+ return new ArrayList<>();
+ }
+
+ Map svcMetadata = new HashMap<>();
+ if (this.properties.getMetadata() != null) {
+ if (this.properties.getMetadata().isAddLabels()) {
+ if (service.getMetadata().getLabels() != null) {
+ String labelPrefix = this.properties.getMetadata().getLabelsPrefix() != null
+ ? this.properties.getMetadata().getLabelsPrefix() : "";
+ service.getMetadata().getLabels().entrySet().stream()
+ .filter(e -> e.getKey().startsWith(labelPrefix))
+ .forEach(e -> svcMetadata.put(e.getKey(), e.getValue()));
+ }
+ }
+ if (this.properties.getMetadata().isAddAnnotations()) {
+ if (service.getMetadata().getAnnotations() != null) {
+ String annotationPrefix = this.properties.getMetadata().getAnnotationsPrefix() != null
+ ? this.properties.getMetadata().getAnnotationsPrefix() : "";
+ service.getMetadata().getAnnotations().entrySet().stream()
+ .filter(e -> e.getKey().startsWith(annotationPrefix))
+ .forEach(e -> svcMetadata.put(e.getKey(), e.getValue()));
+ }
+ }
+ }
+
+ V1Endpoints ep = this.endpointsLister.namespace(service.getMetadata().getNamespace())
+ .get(service.getMetadata().getName());
+ if (ep == null) {
+ // no available endpoints in the cluster
+ return new ArrayList<>();
+ }
+ return ep.getSubsets().stream().flatMap(subset -> {
+ Map metadata = new HashMap<>(svcMetadata);
+ if (this.properties.getMetadata() != null && this.properties.getMetadata().isAddPorts()) {
+ subset.getPorts().stream().forEach(p -> metadata.put(p.getName(), Integer.toString(p.getPort())));
+ }
+ V1EndpointPort port = subset.getPorts() != null && subset.getPorts().size() == 1 ? subset.getPorts().get(0)
+ : subset.getPorts().stream()
+ .filter(p -> this.properties.getPrimaryPortName().equalsIgnoreCase(p.getName())).findFirst()
+ .orElseThrow(IllegalStateException::new);
+ return subset.getAddresses().stream()
+ .map(addr -> new KubernetesServiceInstance(
+ addr.getTargetRef() != null ? addr.getTargetRef().getUid() : "", serviceId, addr.getIp(),
+ port.getPort(), metadata, false));
+ }).collect(Collectors.toList());
+ }
+
+ @Override
+ public List getServices() {
+ List services = this.properties.isAllNamespaces() ? this.serviceLister.list()
+ : this.serviceLister.namespace(this.namespace).list();
+ return services.stream().map(s -> s.getMetadata().getName()).collect(Collectors.toList());
+ }
+
+ @Override
+ public void afterPropertiesSet() throws Exception {
+ this.sharedInformerFactory.startAllRegisteredInformers();
+ if (!Wait.poll(Duration.ofSeconds(1), Duration
+ .ofSeconds(this.properties.getCacheLoadingTimeoutSeconds()), () -> {
+ log.info("Waiting for the cache of informers to be fully loaded..");
+ return this.informersReadyFunc.get();
+ })) {
+ if (this.properties.isWaitCacheReady()) {
+ throw new IllegalStateException(
+ "Timeout waiting for informers cache to be ready, is the kubernetes service up?");
+ }
+ else {
+ log.warn("Timeout waiting for informers cache to be ready, ignoring the failure because waitForInformerCacheReady property is false");
+ }
+ }
+ log.info("Cache fully loaded (total " + serviceLister.list().size()
+ + " services) , discovery client is now available");
+ }
+
+}
diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesReactiveDiscoveryClientAutoConfiguration.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesReactiveDiscoveryClientAutoConfiguration.java
new file mode 100644
index 00000000..21a03d9b
--- /dev/null
+++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesReactiveDiscoveryClientAutoConfiguration.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2013-2019 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
+ *
+ * https://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.client.discovery;
+
+import io.kubernetes.client.informer.SharedInformer;
+import io.kubernetes.client.informer.SharedInformerFactory;
+import io.kubernetes.client.informer.cache.Lister;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.models.V1Endpoints;
+import io.kubernetes.client.openapi.models.V1EndpointsList;
+import io.kubernetes.client.openapi.models.V1Service;
+import io.kubernetes.client.openapi.models.V1ServiceList;
+import io.kubernetes.client.spring.extended.controller.KubernetesInformerFactoryProcessor;
+import io.kubernetes.client.spring.extended.controller.annotation.GroupVersionResource;
+import io.kubernetes.client.spring.extended.controller.annotation.KubernetesInformer;
+import io.kubernetes.client.spring.extended.controller.annotation.KubernetesInformers;
+
+import org.springframework.boot.autoconfigure.AutoConfigureAfter;
+import org.springframework.boot.autoconfigure.AutoConfigureBefore;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.cloud.client.CommonsClientAutoConfiguration;
+import org.springframework.cloud.client.ConditionalOnBlockingDiscoveryEnabled;
+import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration;
+import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration;
+import org.springframework.cloud.kubernetes.client.discovery.gson.EndpointsTrimmingStrategy;
+import org.springframework.cloud.kubernetes.client.discovery.gson.ServiceTrimmingStrategy;
+import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnKubernetesDiscoveryEnabled
+@AutoConfigureBefore({ SimpleDiscoveryClientAutoConfiguration.class, CommonsClientAutoConfiguration.class })
+@AutoConfigureAfter({ KubernetesClientAutoConfiguration.class })
+public class KubernetesReactiveDiscoveryClientAutoConfiguration {
+
+ @Bean
+ public KubernetesDiscoveryProperties getKubernetesDiscoveryProperties() {
+ return new KubernetesDiscoveryProperties();
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnBlockingDiscoveryEnabled
+ public static class KubernetesInformerDiscoveryConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ public KubernetesInformerFactoryProcessor kubernetesInformerFactoryProcessor() {
+ return new KubernetesInformerFactoryProcessor();
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public CatalogSharedInformerFactory catalogSharedInformerFactory(ApiClient apiClient) {
+ apiClient.getJSON()
+ .setGson(apiClient.getJSON().getGson().newBuilder()
+ .addDeserializationExclusionStrategy(new ServiceTrimmingStrategy())
+ .addDeserializationExclusionStrategy(new EndpointsTrimmingStrategy()).create());
+ return new CatalogSharedInformerFactory();
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public KubernetesInformerDiscoveryClient kubernetesInformerDiscoveryClient(
+ KubernetesClientProperties kubernetesClientProperties,
+ CatalogSharedInformerFactory sharedInformerFactory, Lister serviceLister,
+ Lister endpointsLister, SharedInformer serviceInformer,
+ SharedInformer endpointsInformer, KubernetesDiscoveryProperties properties) {
+ return new KubernetesInformerDiscoveryClient(kubernetesClientProperties.getNamespace(),
+ sharedInformerFactory, serviceLister, endpointsLister, serviceInformer, endpointsInformer,
+ properties);
+ }
+
+ @KubernetesInformers({
+ @KubernetesInformer(apiTypeClass = V1Service.class, apiListTypeClass = V1ServiceList.class,
+ groupVersionResource = @GroupVersionResource(apiGroup = "", apiVersion = "v1",
+ resourcePlural = "services")),
+ @KubernetesInformer(apiTypeClass = V1Endpoints.class, apiListTypeClass = V1EndpointsList.class,
+ groupVersionResource = @GroupVersionResource(apiGroup = "", apiVersion = "v1",
+ resourcePlural = "endpoints")) })
+ class CatalogSharedInformerFactory extends SharedInformerFactory {
+
+ // TODO: optimization to ease memory pressure from continuous list&watch.
+
+ }
+
+ }
+
+}
diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesServiceInstance.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesServiceInstance.java
new file mode 100644
index 00000000..441d20ee
--- /dev/null
+++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesServiceInstance.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2013-2019 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
+ *
+ * https://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.client.discovery;
+
+import java.net.URI;
+import java.util.Map;
+import java.util.Objects;
+
+import org.springframework.cloud.client.ServiceInstance;
+
+public class KubernetesServiceInstance implements ServiceInstance {
+
+ /**
+ * Key of the namespace metadata.
+ */
+ public static final String NAMESPACE_METADATA_KEY = "k8s_namespace";
+
+ private static final String HTTP_PREFIX = "http";
+
+ private static final String HTTPS_PREFIX = "https";
+
+ private static final String DSL = "//";
+
+ private static final String COLON = ":";
+
+ private final String instanceId;
+
+ private final String serviceId;
+
+ private final String host;
+
+ private final int port;
+
+ private final URI uri;
+
+ private final Boolean secure;
+
+ private final Map metadata;
+
+ /**
+ * @param instanceId the id of the instance.
+ * @param serviceId the id of the service.
+ * @param host the address where the service instance can be found.
+ * @param port the port on which the service is running.
+ * @param metadata a map containing metadata.
+ * @param secure indicates whether or not the connection needs to be secure.
+ */
+ public KubernetesServiceInstance(String instanceId, String serviceId, String host, int port,
+ Map metadata, Boolean secure) {
+ this.instanceId = instanceId;
+ this.serviceId = serviceId;
+ this.host = host;
+ this.port = port;
+ this.metadata = metadata;
+ this.secure = secure;
+ this.uri = createUri(secure ? HTTPS_PREFIX : HTTP_PREFIX, host, port);
+ }
+
+ @Override
+ public String getInstanceId() {
+ return this.instanceId;
+ }
+
+ @Override
+ public String getServiceId() {
+ return this.serviceId;
+ }
+
+ @Override
+ public String getHost() {
+ return this.host;
+ }
+
+ @Override
+ public int getPort() {
+ return this.port;
+ }
+
+ @Override
+ public boolean isSecure() {
+ return this.secure;
+ }
+
+ @Override
+ public URI getUri() {
+ return uri;
+ }
+
+ public Map getMetadata() {
+ return this.metadata;
+ }
+
+ @Override
+ public String getScheme() {
+ return isSecure() ? HTTPS_PREFIX : HTTP_PREFIX;
+ }
+
+ private URI createUri(String scheme, String host, int port) {
+ return URI.create(scheme + COLON + DSL + host + COLON + port);
+ }
+
+ public String getNamespace() {
+ return this.metadata != null ? this.metadata.get(NAMESPACE_METADATA_KEY) : null;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ KubernetesServiceInstance that = (KubernetesServiceInstance) o;
+ return port == that.port && Objects.equals(instanceId, that.instanceId)
+ && Objects.equals(serviceId, that.serviceId) && Objects.equals(host, that.host)
+ && Objects.equals(uri, that.uri) && Objects.equals(secure, that.secure)
+ && Objects.equals(metadata, that.metadata);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(instanceId, serviceId, host, port, uri, secure, metadata);
+ }
+
+}
diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/gson/EndpointsTrimmingStrategy.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/gson/EndpointsTrimmingStrategy.java
new file mode 100644
index 00000000..069b1b79
--- /dev/null
+++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/gson/EndpointsTrimmingStrategy.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2013-2019 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
+ *
+ * https://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.client.discovery.gson;
+
+import com.google.gson.ExclusionStrategy;
+import com.google.gson.FieldAttributes;
+import io.kubernetes.client.openapi.models.V1ObjectMeta;
+
+public class EndpointsTrimmingStrategy implements ExclusionStrategy {
+
+ @Override
+ public boolean shouldSkipField(FieldAttributes fieldAttributes) {
+ // trimming field-managers
+ if (V1ObjectMeta.class.equals(fieldAttributes.getDeclaringClass())) {
+ return "managedFields".equals(fieldAttributes.getName());
+ }
+ return false;
+ }
+
+ @Override
+ public boolean shouldSkipClass(Class> aClass) {
+ return false;
+ }
+
+}
diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/gson/ServiceTrimmingStrategy.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/gson/ServiceTrimmingStrategy.java
new file mode 100644
index 00000000..9b6f4fec
--- /dev/null
+++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/gson/ServiceTrimmingStrategy.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2013-2019 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
+ *
+ * https://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.client.discovery.gson;
+
+import com.google.gson.ExclusionStrategy;
+import com.google.gson.FieldAttributes;
+import io.kubernetes.client.openapi.models.V1ObjectMeta;
+import io.kubernetes.client.openapi.models.V1ServiceSpec;
+import io.kubernetes.client.openapi.models.V1ServiceStatus;
+
+public class ServiceTrimmingStrategy implements ExclusionStrategy {
+
+ @Override
+ public boolean shouldSkipField(FieldAttributes fieldAttributes) {
+ // trimming field-managers
+ if (V1ObjectMeta.class.equals(fieldAttributes.getDeclaringClass())) {
+ return "managedFields".equals(fieldAttributes.getName());
+ }
+ return false;
+ }
+
+ @Override
+ public boolean shouldSkipClass(Class> aClass) {
+ if (V1ServiceSpec.class.equals(aClass)) {
+ return true;
+ }
+ return V1ServiceStatus.class.equals(aClass);
+ }
+
+}
diff --git a/spring-cloud-kubernetes-client-discovery/src/main/resources/META-INF/spring.factories b/spring-cloud-kubernetes-client-discovery/src/main/resources/META-INF/spring.factories
new file mode 100644
index 00000000..2b627c2f
--- /dev/null
+++ b/spring-cloud-kubernetes-client-discovery/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,6 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+org.springframework.cloud.kubernetes.client.discovery.KubernetesReactiveDiscoveryClientAutoConfiguration
+
+org.springframework.cloud.bootstrap.BootstrapConfiguration=\
+org.springframework.cloud.kubernetes.client.discovery.KubernetesDiscoveryClientConfigClientBootstrapConfiguration
+
diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientConfigClientBootstrapConfigurationTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientConfigClientBootstrapConfigurationTests.java
new file mode 100644
index 00000000..4e9b5758
--- /dev/null
+++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientConfigClientBootstrapConfigurationTests.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2013-2019 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
+ *
+ * https://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.client.discovery;
+
+import java.util.Collections;
+
+import org.junit.After;
+import org.junit.Test;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
+import org.springframework.boot.test.util.TestPropertyValues;
+import org.springframework.cloud.client.DefaultServiceInstance;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.discovery.DiscoveryClient;
+import org.springframework.cloud.commons.util.UtilAutoConfiguration;
+import org.springframework.cloud.config.client.ConfigClientProperties;
+import org.springframework.cloud.config.client.DiscoveryClientConfigServiceBootstrapConfiguration;
+import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration;
+import org.springframework.cloud.kubernetes.commons.KubernetesCommonsAutoConfiguration;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+import static junit.framework.TestCase.assertEquals;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnProperty("spring.cloud.config.discovery.enabled")
+@Import({ KubernetesClientAutoConfiguration.class, KubernetesReactiveDiscoveryClientAutoConfiguration.class })
+public class KubernetesDiscoveryClientConfigClientBootstrapConfigurationTests {
+
+ private AnnotationConfigApplicationContext context;
+
+ @After
+ public void close() {
+ if (this.context != null) {
+ if (this.context.getParent() != null) {
+ ((AnnotationConfigApplicationContext) this.context.getParent()).close();
+ }
+ this.context.close();
+ }
+ }
+
+ @Test
+ public void onWhenRequested() throws Exception {
+ setup("server.port=7000", "spring.cloud.config.discovery.enabled=true",
+ "spring.cloud.kubernetes.discovery.enabled:true", "spring.cloud.kubernetes.enabled:true",
+ "spring.application.name:test", "spring.cloud.config.discovery.service-id:configserver");
+ assertEquals(1, this.context.getParent().getBeanNamesForType(DiscoveryClient.class).length);
+
+ DiscoveryClient client = this.context.getParent().getBean(DiscoveryClient.class);
+ verify(client, atLeast(2)).getInstances("configserver");
+ ConfigClientProperties locator = this.context.getBean(ConfigClientProperties.class);
+ assertEquals("http://fake:8888/", locator.getUri()[0]);
+ }
+
+ private void setup(String... env) {
+ AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext();
+ TestPropertyValues.of(env).applyTo(parent);
+ parent.register(UtilAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class,
+ EnvironmentKnobbler.class, KubernetesCommonsAutoConfiguration.class,
+ KubernetesClientAutoConfiguration.class, KubernetesReactiveDiscoveryClientAutoConfiguration.class,
+ DiscoveryClientConfigServiceBootstrapConfiguration.class, ConfigClientProperties.class);
+ parent.refresh();
+ this.context = new AnnotationConfigApplicationContext();
+ this.context.setParent(parent);
+ this.context.register(PropertyPlaceholderAutoConfiguration.class, KubernetesCommonsAutoConfiguration.class,
+ KubernetesReactiveDiscoveryClientAutoConfiguration.class);
+ this.context.refresh();
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ protected static class EnvironmentKnobbler {
+
+ @Bean
+ public KubernetesInformerDiscoveryClient kubernetesInformerDiscoveryClient() {
+ KubernetesInformerDiscoveryClient client = mock(KubernetesInformerDiscoveryClient.class);
+ ServiceInstance instance = new DefaultServiceInstance("configserver1", "configserver", "fake", 8888, false);
+ given(client.getInstances("configserver")).willReturn(Collections.singletonList(instance));
+ return client;
+ }
+
+ }
+
+}
diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientTests.java
new file mode 100644
index 00000000..d6f3eb01
--- /dev/null
+++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientTests.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2013-2019 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
+ *
+ * https://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.client.discovery;
+
+import java.util.HashMap;
+
+import io.kubernetes.client.informer.SharedInformerFactory;
+import io.kubernetes.client.informer.cache.Cache;
+import io.kubernetes.client.informer.cache.Lister;
+import io.kubernetes.client.openapi.models.V1EndpointAddress;
+import io.kubernetes.client.openapi.models.V1EndpointPort;
+import io.kubernetes.client.openapi.models.V1EndpointSubset;
+import io.kubernetes.client.openapi.models.V1Endpoints;
+import io.kubernetes.client.openapi.models.V1ObjectMeta;
+import io.kubernetes.client.openapi.models.V1Service;
+import io.kubernetes.client.openapi.models.V1ServiceSpec;
+import io.kubernetes.client.openapi.models.V1ServiceStatus;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class KubernetesInformerDiscoveryClientTests {
+
+ @Mock
+ private SharedInformerFactory sharedInformerFactory;
+
+ @Mock
+ private KubernetesDiscoveryProperties kubernetesDiscoveryProperties;
+
+ private static final V1Service testService1 = new V1Service()
+ .metadata(new V1ObjectMeta().name("test-svc-1").namespace("namespace1"))
+ .spec(new V1ServiceSpec().loadBalancerIP("1.1.1.1")).status(new V1ServiceStatus());
+
+ private static final V1Service testService2 = new V1Service()
+ .metadata(new V1ObjectMeta().name("test-svc-1").namespace("namespace2"))
+ .spec(new V1ServiceSpec().loadBalancerIP("1.1.1.1")).status(new V1ServiceStatus());
+
+ private static final V1Endpoints testEndpoints1 = new V1Endpoints()
+ .metadata(new V1ObjectMeta().name("test-svc-1").namespace("namespace1"))
+ .addSubsetsItem(new V1EndpointSubset().addPortsItem(new V1EndpointPort().port(8080))
+ .addAddressesItem(new V1EndpointAddress().ip("2.2.2.2")));
+
+ @Test
+ public void testDiscoveryGetServicesAllNamespaceShouldWork() {
+ Lister serviceLister = setupServiceLister(testService1, testService2);
+
+ when(kubernetesDiscoveryProperties.isAllNamespaces()).thenReturn(true);
+
+ KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("",
+ sharedInformerFactory, serviceLister, null, null, null, kubernetesDiscoveryProperties);
+
+ assertThat(discoveryClient.getServices().toArray())
+ .containsOnly(testService1.getMetadata().getName(), testService2.getMetadata().getName());
+
+ verify(kubernetesDiscoveryProperties, times(1)).isAllNamespaces();
+ }
+
+ @Test
+ public void testDiscoveryGetServicesOneNamespaceShouldWork() {
+ Lister serviceLister = setupServiceLister(testService1, testService2);
+
+ when(kubernetesDiscoveryProperties.isAllNamespaces()).thenReturn(false);
+
+ KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("namespace1",
+ sharedInformerFactory, serviceLister, null, null, null, kubernetesDiscoveryProperties);
+
+ assertThat(discoveryClient.getServices().toArray())
+ .containsOnly(testService1.getMetadata().getName());
+
+ verify(kubernetesDiscoveryProperties, times(1)).isAllNamespaces();
+ }
+
+ @Test
+ public void testDiscoveryGetInstanceAllNamespaceShouldWork() {
+ Lister serviceLister = setupServiceLister(testService1, testService2);
+ Lister endpointsLister = setupEndpointsLister(testEndpoints1);
+
+ when(kubernetesDiscoveryProperties.isAllNamespaces()).thenReturn(true);
+
+ KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("",
+ sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
+
+ assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly(
+ new KubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, new HashMap<>(), false));
+
+ verify(kubernetesDiscoveryProperties, times(1)).isAllNamespaces();
+ }
+
+ @Test
+ public void testDiscoveryGetInstanceOneNamespaceShouldWork() {
+ Lister serviceLister = setupServiceLister(testService1, testService2);
+ Lister endpointsLister = setupEndpointsLister(testEndpoints1);
+
+ when(kubernetesDiscoveryProperties.isAllNamespaces()).thenReturn(false);
+
+ KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("namespace1",
+ sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
+
+ assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly(
+ new KubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, new HashMap<>(), false));
+ verify(kubernetesDiscoveryProperties, times(1)).isAllNamespaces();
+ }
+
+ private Lister setupServiceLister(V1Service... services) {
+ Cache serviceCache = new Cache<>();
+ Lister serviceLister = new Lister<>(serviceCache);
+ for (V1Service svc : services) {
+ serviceCache.add(svc);
+ }
+ return serviceLister;
+ }
+
+ private Lister setupEndpointsLister(V1Endpoints... endpoints) {
+ Cache endpointsCache = new Cache<>();
+ Lister endpointsLister = new Lister<>(endpointsCache);
+ for (V1Endpoints ep : endpoints) {
+ endpointsCache.add(ep);
+ }
+ return endpointsLister;
+ }
+
+}
diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesReactiveDiscoveryClientAutoConfigurationTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesReactiveDiscoveryClientAutoConfigurationTests.java
new file mode 100644
index 00000000..96c19b42
--- /dev/null
+++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesReactiveDiscoveryClientAutoConfigurationTests.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2013-2019 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
+ *
+ * https://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.client.discovery;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.cloud.client.discovery.DiscoveryClient;
+import org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClient;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = {
+ "spring.cloud.kubernetes.discovery.cacheLoadingTimeoutSeconds=5",
+ "spring.cloud.kubernetes.discovery.waitCacheReady=false"
+})
+public class KubernetesReactiveDiscoveryClientAutoConfigurationTests {
+
+ @Autowired(required = false)
+ private DiscoveryClient discoveryClient;
+
+ @Test
+ public void kubernetesDiscoveryClientCreated() {
+ assertThat(this.discoveryClient).isNotNull().isInstanceOf(CompositeDiscoveryClient.class);
+
+ CompositeDiscoveryClient composite = (CompositeDiscoveryClient) this.discoveryClient;
+ assertThat(composite.getDiscoveryClients().stream()
+ .anyMatch(dc -> dc instanceof KubernetesInformerDiscoveryClient)).isTrue();
+ }
+
+ @SpringBootApplication
+ protected static class TestConfig {
+
+ }
+
+}
diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesServiceInstanceTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesServiceInstanceTests.java
new file mode 100644
index 00000000..d2acd3a4
--- /dev/null
+++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesServiceInstanceTests.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2013-2020 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
+ *
+ * https://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.client.discovery;
+
+import java.util.Collections;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class KubernetesServiceInstanceTests {
+
+ @Test
+ public void schemeIsHttp() {
+ assertServiceInstance(false);
+ }
+
+ private KubernetesServiceInstance assertServiceInstance(boolean secure) {
+ KubernetesServiceInstance instance = new KubernetesServiceInstance("123", "myservice", "1.2.3.4", 8080,
+ Collections.emptyMap(), secure);
+
+ assertThat(instance.getInstanceId()).isEqualTo("123");
+ assertThat(instance.getServiceId()).isEqualTo("myservice");
+ assertThat(instance.getHost()).isEqualTo("1.2.3.4");
+ assertThat(instance.getPort()).isEqualTo(8080);
+ assertThat(instance.isSecure()).isEqualTo(secure);
+ assertThat(instance.getScheme()).isEqualTo(secure ? "https" : "http");
+ return instance;
+ }
+
+ @Test
+ public void schemeIsHttps() {
+ assertServiceInstance(true);
+ }
+
+}
diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/gson/EndpointsTrimmingStrategyTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/gson/EndpointsTrimmingStrategyTests.java
new file mode 100644
index 00000000..c7fed980
--- /dev/null
+++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/gson/EndpointsTrimmingStrategyTests.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2013-2019 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
+ *
+ * https://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.client.discovery.gson;
+
+import java.util.Arrays;
+
+import com.google.gson.Gson;
+import io.kubernetes.client.openapi.models.V1Endpoints;
+import io.kubernetes.client.openapi.models.V1ManagedFieldsEntry;
+import io.kubernetes.client.openapi.models.V1ObjectMeta;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class EndpointsTrimmingStrategyTests {
+
+ @Test
+ public void testDeserializingEndpoints() {
+ Gson gson = new Gson().newBuilder().addDeserializationExclusionStrategy(new EndpointsTrimmingStrategy())
+ .create();
+ V1Endpoints input = new V1Endpoints()
+ .metadata(new V1ObjectMeta().name("foo").managedFields(Arrays.asList(new V1ManagedFieldsEntry())));
+
+ String data = gson.toJson(input);
+ V1Endpoints output = gson.fromJson(data, V1Endpoints.class);
+
+ // managed-fields should be excluded
+ Assert.assertNull(output.getMetadata().getManagedFields());
+
+ }
+
+}
diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/gson/ServiceTrimmingStrategyTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/gson/ServiceTrimmingStrategyTests.java
new file mode 100644
index 00000000..6a5657eb
--- /dev/null
+++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/gson/ServiceTrimmingStrategyTests.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2013-2019 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
+ *
+ * https://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.client.discovery.gson;
+
+import java.util.Arrays;
+
+import com.google.gson.Gson;
+import io.kubernetes.client.openapi.models.V1LoadBalancerIngress;
+import io.kubernetes.client.openapi.models.V1LoadBalancerStatus;
+import io.kubernetes.client.openapi.models.V1ManagedFieldsEntry;
+import io.kubernetes.client.openapi.models.V1ObjectMeta;
+import io.kubernetes.client.openapi.models.V1Service;
+import io.kubernetes.client.openapi.models.V1ServiceSpec;
+import io.kubernetes.client.openapi.models.V1ServiceStatus;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ServiceTrimmingStrategyTests {
+
+ @Test
+ public void testDeserializingService() {
+ Gson gson = new Gson().newBuilder().addDeserializationExclusionStrategy(new ServiceTrimmingStrategy()).create();
+ V1Service input = new V1Service()
+ .metadata(new V1ObjectMeta().name("foo").managedFields(Arrays.asList(new V1ManagedFieldsEntry())))
+ .spec(new V1ServiceSpec().loadBalancerIP("1.1.1.1")).status(new V1ServiceStatus().loadBalancer(
+ new V1LoadBalancerStatus().addIngressItem(new V1LoadBalancerIngress().ip("2.2.2.2"))));
+ String data = gson.toJson(input);
+ V1Service output = gson.fromJson(data, V1Service.class);
+
+ // spec should be excluded
+ Assert.assertNull(output.getSpec());
+ // status should be excluded
+ Assert.assertNull(output.getStatus());
+ // managed-fields should be excluded
+ Assert.assertNull(output.getMetadata().getManagedFields());
+ }
+
+}
diff --git a/spring-cloud-kubernetes-dependencies/pom.xml b/spring-cloud-kubernetes-dependencies/pom.xml
index 84c9931a..d6a39641 100644
--- a/spring-cloud-kubernetes-dependencies/pom.xml
+++ b/spring-cloud-kubernetes-dependencies/pom.xml
@@ -61,6 +61,11 @@
client-java-extended
${kubernetes-java-client.version}
+
+ io.kubernetes
+ client-java-spring-integration
+ ${kubernetes-java-client.version}
+
me.snowdrop
diff --git a/spring-cloud-kubernetes-integration-tests/discovery/kubernetes-client-discovery/pom.xml b/spring-cloud-kubernetes-integration-tests/discovery/kubernetes-client-discovery/pom.xml
new file mode 100644
index 00000000..89a6de82
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/discovery/kubernetes-client-discovery/pom.xml
@@ -0,0 +1,32 @@
+
+
+ 4.0.0
+
+
+ org.springframework.cloud
+ discovery-parent
+ 2.0.0-SNAPSHOT
+
+
+ kubernetes-client-discovery
+ Spring Cloud Kubernetes :: Integration Tests :: Kubernetes Client Discovery
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.cloud
+ spring-cloud-kubernetes-client-discovery
+ ${project.version}
+
+
+
+
diff --git a/spring-cloud-kubernetes-integration-tests/discovery/kubernetes-client-discovery/src/main/fabric8/deployment.yml b/spring-cloud-kubernetes-integration-tests/discovery/kubernetes-client-discovery/src/main/fabric8/deployment.yml
new file mode 100644
index 00000000..f40eb7e9
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/discovery/kubernetes-client-discovery/src/main/fabric8/deployment.yml
@@ -0,0 +1,10 @@
+# We need this fragment in order for the kubernetes client to talk to
+# the Kubernetes API without caring about proper certificates
+spec:
+ template:
+ spec:
+ containers:
+ - env:
+ - name: KUBERNETES_TRUST_CERTIFICATES
+ value: true
+
diff --git a/spring-cloud-kubernetes-integration-tests/discovery/kubernetes-client-discovery/src/main/fabric8/svc.yml b/spring-cloud-kubernetes-integration-tests/discovery/kubernetes-client-discovery/src/main/fabric8/svc.yml
new file mode 100644
index 00000000..945f430d
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/discovery/kubernetes-client-discovery/src/main/fabric8/svc.yml
@@ -0,0 +1,15 @@
+# we are using an FMP fragment to ensure that NodePort is used correctly
+kind: Service
+apiVersion: v1
+metadata:
+ name: ${project.artifactId}
+ labels:
+ app: ${project.artifactId}
+spec:
+ selector:
+ app: ${project.artifactId}
+ ports:
+ - protocol: TCP
+ port: 8080
+ nodePort: ${nodeport.value}
+ type: NodePort
diff --git a/spring-cloud-kubernetes-integration-tests/discovery/kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/it/DiscoveryClientApplication.java b/spring-cloud-kubernetes-integration-tests/discovery/kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/it/DiscoveryClientApplication.java
new file mode 100644
index 00000000..ef12e9ab
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/discovery/kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/it/DiscoveryClientApplication.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2013-2019 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
+ *
+ * https://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.it;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.discovery.DiscoveryClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RestController;
+
+@SpringBootApplication
+@RestController
+public class DiscoveryClientApplication {
+
+ @Autowired
+ private DiscoveryClient discoveryClient;
+
+ public static void main(String[] args) {
+ SpringApplication.run(DiscoveryClientApplication.class, args);
+ }
+
+ @GetMapping("/services")
+ public List services() {
+ return this.discoveryClient.getServices();
+ }
+
+ @GetMapping("/services/{service}/instances")
+ public List instances(@PathVariable("service") String service) {
+ return this.discoveryClient.getInstances(service);
+ }
+
+}
diff --git a/spring-cloud-kubernetes-integration-tests/discovery/pom.xml b/spring-cloud-kubernetes-integration-tests/discovery/pom.xml
index bbef858b..5f85cc12 100644
--- a/spring-cloud-kubernetes-integration-tests/discovery/pom.xml
+++ b/spring-cloud-kubernetes-integration-tests/discovery/pom.xml
@@ -18,6 +18,7 @@
discovery-service-a
discovery-service-b
discovery-client
+ kubernetes-client-discovery
tests