From 3794e0e9a2d959ac2a86149e41e74f767341e250 Mon Sep 17 00:00:00 2001 From: yue9944882 <291271447@qq.com> Date: Fri, 6 Nov 2020 15:20:08 +0800 Subject: [PATCH] informer-based service discovery --- pom.xml | 1 + .../pom.xml | 4 + .../KubernetesClientAutoConfiguration.java | 10 +- .../pom.xml | 61 ++++ ...nditionalOnKubernetesDiscoveryEnabled.java | 35 +++ ...entConfigClientBootstrapConfiguration.java | 29 ++ .../KubernetesDiscoveryProperties.java | 275 ++++++++++++++++++ .../KubernetesInformerDiscoveryClient.java | 160 ++++++++++ ...ctiveDiscoveryClientAutoConfiguration.java | 103 +++++++ .../discovery/KubernetesServiceInstance.java | 140 +++++++++ .../gson/EndpointsTrimmingStrategy.java | 39 +++ .../gson/ServiceTrimmingStrategy.java | 44 +++ .../main/resources/META-INF/spring.factories | 6 + ...nfigClientBootstrapConfigurationTests.java | 104 +++++++ ...ubernetesInformerDiscoveryClientTests.java | 143 +++++++++ ...DiscoveryClientAutoConfigurationTests.java | 55 ++++ .../KubernetesServiceInstanceTests.java | 50 ++++ .../gson/EndpointsTrimmingStrategyTests.java | 45 +++ .../gson/ServiceTrimmingStrategyTests.java | 52 ++++ spring-cloud-kubernetes-dependencies/pom.xml | 5 + .../kubernetes-client-discovery/pom.xml | 32 ++ .../src/main/fabric8/deployment.yml | 10 + .../src/main/fabric8/svc.yml | 15 + .../it/DiscoveryClientApplication.java | 51 ++++ .../discovery/pom.xml | 1 + 25 files changed, 1468 insertions(+), 2 deletions(-) create mode 100644 spring-cloud-kubernetes-client-discovery/pom.xml create mode 100644 spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/ConditionalOnKubernetesDiscoveryEnabled.java create mode 100644 spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientConfigClientBootstrapConfiguration.java create mode 100644 spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryProperties.java create mode 100644 spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClient.java create mode 100644 spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesReactiveDiscoveryClientAutoConfiguration.java create mode 100644 spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesServiceInstance.java create mode 100644 spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/gson/EndpointsTrimmingStrategy.java create mode 100644 spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/gson/ServiceTrimmingStrategy.java create mode 100644 spring-cloud-kubernetes-client-discovery/src/main/resources/META-INF/spring.factories create mode 100644 spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientConfigClientBootstrapConfigurationTests.java create mode 100644 spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientTests.java create mode 100644 spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesReactiveDiscoveryClientAutoConfigurationTests.java create mode 100644 spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesServiceInstanceTests.java create mode 100644 spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/gson/EndpointsTrimmingStrategyTests.java create mode 100644 spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/gson/ServiceTrimmingStrategyTests.java create mode 100644 spring-cloud-kubernetes-integration-tests/discovery/kubernetes-client-discovery/pom.xml create mode 100644 spring-cloud-kubernetes-integration-tests/discovery/kubernetes-client-discovery/src/main/fabric8/deployment.yml create mode 100644 spring-cloud-kubernetes-integration-tests/discovery/kubernetes-client-discovery/src/main/fabric8/svc.yml create mode 100644 spring-cloud-kubernetes-integration-tests/discovery/kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/it/DiscoveryClientApplication.java diff --git a/pom.xml b/pom.xml index ad2adf27..8e8e2dad 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 3277b9c7..d7da9714 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