diff --git a/docs/src/main/asciidoc/load-balancer.adoc b/docs/src/main/asciidoc/load-balancer.adoc index 33f5333d..31a14c53 100644 --- a/docs/src/main/asciidoc/load-balancer.adoc +++ b/docs/src/main/asciidoc/load-balancer.adoc @@ -2,6 +2,7 @@ This project includes Spring Cloud Load Balancer for load balancing based on Kubernetes Endpoints and provides implementation of load balancer based on Kubernetes Service. To include it to your project add the following dependency. ==== +Fabric8 Implementation [source,xml] ---- @@ -11,6 +12,17 @@ To include it to your project add the following dependency. ---- ==== +==== +Kubernetes Java Client Implementation +[source,xml] +---- + + org.springframework.cloud + spring-cloud-starter-kubernetes-client-loadbalancer + +---- +==== + To enable load balancing based on Kubernetes Service name use the following property. Then load balancer would try to call application using address, for example `service-a.default.svc.cluster.local` ==== [source] diff --git a/pom.xml b/pom.xml index d4cf956e..edd0815a 100644 --- a/pom.xml +++ b/pom.xml @@ -90,6 +90,7 @@ spring-cloud-kubernetes-test-support spring-cloud-kubernetes-client-autoconfig spring-cloud-kubernetes-client-config + spring-cloud-kubernetes-client-loadbalancer spring-cloud-kubernetes-fabric8-autoconfig spring-cloud-kubernetes-fabric8-config spring-cloud-kubernetes-fabric8-discovery @@ -99,6 +100,7 @@ spring-cloud-starter-kubernetes-fabric8-all spring-cloud-starter-kubernetes-client spring-cloud-starter-kubernetes-client-config + spring-cloud-starter-kubernetes-client-loadbalancer spring-cloud-starter-kubernetes-client-all spring-cloud-kubernetes-examples spring-cloud-kubernetes-fabric8-leader diff --git a/spring-cloud-kubernetes-client-config/pom.xml b/spring-cloud-kubernetes-client-config/pom.xml index 09ca59cf..47fc746c 100644 --- a/spring-cloud-kubernetes-client-config/pom.xml +++ b/spring-cloud-kubernetes-client-config/pom.xml @@ -11,11 +11,6 @@ spring-cloud-kubernetes-client-config - - 2.26.3 - - - org.springframework.cloud @@ -81,7 +76,6 @@ com.github.tomakehurst wiremock-jre8 - ${wiremock.version} test diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientAutoConfiguration.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientAutoConfiguration.java index 60992f26..82fda1b1 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientAutoConfiguration.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientAutoConfiguration.java @@ -37,8 +37,6 @@ 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.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; import org.springframework.context.annotation.Bean; @@ -64,10 +62,6 @@ public class KubernetesDiscoveryClientAutoConfiguration { @Bean @ConditionalOnMissingBean public CatalogSharedInformerFactory catalogSharedInformerFactory(ApiClient apiClient) { - apiClient.getJSON() - .setGson(apiClient.getJSON().getGson().newBuilder() - .addDeserializationExclusionStrategy(new ServiceTrimmingStrategy()) - .addDeserializationExclusionStrategy(new EndpointsTrimmingStrategy()).create()); return new CatalogSharedInformerFactory(); } 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 index ac3fa8e6..4d7d77e1 100644 --- 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 @@ -40,6 +40,7 @@ import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesServiceInstance; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; public class KubernetesInformerDiscoveryClient implements DiscoveryClient, InitializingBean { @@ -80,6 +81,10 @@ public class KubernetesInformerDiscoveryClient implements DiscoveryClient, Initi public List getInstances(String serviceId) { Assert.notNull(serviceId, "[Assertion failed] - the object argument must not be null"); + if (StringUtils.hasText(namespace) && !properties.isAllNamespaces()) { + log.warn("Namespace is null or empty, this may cause issues looking up services"); + } + 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); 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 index 24e257e6..c8d64580 100644 --- 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 @@ -121,7 +121,7 @@ public class KubernetesInformerDiscoveryClientTests { 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(); + verify(kubernetesDiscoveryProperties, times(2)).isAllNamespaces(); } private Lister setupServiceLister(V1Service... services) { diff --git a/spring-cloud-kubernetes-client-loadbalancer/pom.xml b/spring-cloud-kubernetes-client-loadbalancer/pom.xml new file mode 100644 index 00000000..e385fee2 --- /dev/null +++ b/spring-cloud-kubernetes-client-loadbalancer/pom.xml @@ -0,0 +1,61 @@ + + + + spring-cloud-kubernetes + org.springframework.cloud + 2.0.0-SNAPSHOT + + 4.0.0 + + spring-cloud-kubernetes-client-loadbalancer + + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + org.springframework.cloud + spring-cloud-kubernetes-client-discovery + + + org.springframework.boot + spring-boot-starter-actuator + true + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter-test + test + + + com.github.tomakehurst + wiremock-jre8 + test + + + io.projectreactor + reactor-test + test + + + org.springframework.boot + spring-boot-starter-web + test + + + org.junit.vintage + junit-vintage-engine + test + + + + + diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerAutoConfiguration.java b/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerAutoConfiguration.java new file mode 100644 index 00000000..6d9f4561 --- /dev/null +++ b/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerAutoConfiguration.java @@ -0,0 +1,44 @@ +/* + * 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.loadbalancer; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.kubernetes.commons.ConditionalOnKubernetesEnabled; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesLoadBalancerProperties; +import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Ryan Baxter + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(KubernetesLoadBalancerProperties.class) +@ConditionalOnKubernetesEnabled +@ConditionalOnProperty(value = "spring.cloud.kubernetes.loadbalancer.enabled", matchIfMissing = true) +@LoadBalancerClients(defaultConfiguration = KubernetesClientLoadBalancerClientConfiguration.class) +public class KubernetesClientLoadBalancerAutoConfiguration { + + @Bean + KubernetesClientServiceInstanceMapper mapper(KubernetesLoadBalancerProperties properties, + KubernetesDiscoveryProperties discoveryProperties) { + return new KubernetesClientServiceInstanceMapper(properties, discoveryProperties); + } + +} diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerClientConfiguration.java b/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerClientConfiguration.java new file mode 100644 index 00000000..9dfba232 --- /dev/null +++ b/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerClientConfiguration.java @@ -0,0 +1,42 @@ +/* + * 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.loadbalancer; + +import io.kubernetes.client.openapi.apis.CoreV1Api; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServicesListSupplier; +import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; + +/** + * @author Ryan Baxter + */ +public class KubernetesClientLoadBalancerClientConfiguration { + + @Bean + @ConditionalOnProperty(name = "spring.cloud.kubernetes.loadbalancer.mode", havingValue = "SERVICE") + KubernetesServicesListSupplier kubernetesServicesListSupplier(Environment environment, CoreV1Api coreV1Api, + KubernetesClientServiceInstanceMapper mapper, KubernetesDiscoveryProperties discoveryProperties, + KubernetesClientProperties kubernetesClientProperties) { + return new KubernetesClientServicesListSupplier(environment, mapper, discoveryProperties, coreV1Api, + kubernetesClientProperties); + } + +} diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientServiceInstanceMapper.java b/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientServiceInstanceMapper.java new file mode 100644 index 00000000..25bd5aa0 --- /dev/null +++ b/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientServiceInstanceMapper.java @@ -0,0 +1,93 @@ +/* + * 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.loadbalancer; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServicePort; + +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesServiceInstance; +import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesLoadBalancerProperties; +import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServiceInstanceMapper; +import org.springframework.util.StringUtils; + +/** + * @author Ryan Baxter + */ +public class KubernetesClientServiceInstanceMapper implements KubernetesServiceInstanceMapper { + + private KubernetesLoadBalancerProperties properties; + + private KubernetesDiscoveryProperties discoveryProperties; + + public KubernetesClientServiceInstanceMapper(KubernetesLoadBalancerProperties properties, + KubernetesDiscoveryProperties discoveryProperties) { + this.properties = properties; + this.discoveryProperties = discoveryProperties; + } + + @Override + public KubernetesServiceInstance map(V1Service service) { + final V1ObjectMeta meta = service.getMetadata(); + + final List ports = service.getSpec().getPorts(); + V1ServicePort port = null; + if (ports.size() == 1) { + port = ports.get(0); + } + else if (ports.size() > 1 && StringUtils.hasText(this.properties.getPortName())) { + Optional optPort = ports.stream() + .filter(it -> properties.getPortName().endsWith(it.getName())).findAny(); + if (optPort.isPresent()) { + port = optPort.get(); + } + } + if (port == null) { + return null; + } + final String host = KubernetesServiceInstanceMapper.createHost(service.getMetadata().getName(), + service.getMetadata().getNamespace(), properties.getClusterDomain()); + final boolean secure = KubernetesServiceInstanceMapper.isSecure(service.getMetadata().getLabels(), + service.getMetadata().getAnnotations(), port.getName(), port.getPort()); + return new KubernetesServiceInstance(meta.getUid(), meta.getName(), host, port.getPort(), + getServiceMetadata(service), secure); + } + + private Map getServiceMetadata(V1Service service) { + final Map serviceMetadata = new HashMap<>(); + KubernetesDiscoveryProperties.Metadata metadataProps = this.discoveryProperties.getMetadata(); + if (metadataProps.isAddLabels()) { + Map labelMetadata = KubernetesServiceInstanceMapper + .getMapWithPrefixedKeys(service.getMetadata().getLabels(), metadataProps.getLabelsPrefix()); + serviceMetadata.putAll(labelMetadata); + } + if (metadataProps.isAddAnnotations()) { + Map annotationMetadata = KubernetesServiceInstanceMapper.getMapWithPrefixedKeys( + service.getMetadata().getAnnotations(), metadataProps.getAnnotationsPrefix()); + serviceMetadata.putAll(annotationMetadata); + } + + return serviceMetadata; + } + +} diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientServicesListSupplier.java b/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientServicesListSupplier.java new file mode 100644 index 00000000..ec1b801f --- /dev/null +++ b/spring-cloud-kubernetes-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientServicesListSupplier.java @@ -0,0 +1,78 @@ +/* + * 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.loadbalancer; + +import java.util.ArrayList; +import java.util.List; + +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1Service; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Flux; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServiceInstanceMapper; +import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServicesListSupplier; +import org.springframework.core.env.Environment; + +/** + * @author Ryan Baxter + */ +public class KubernetesClientServicesListSupplier extends KubernetesServicesListSupplier { + + private static final Log LOG = LogFactory.getLog(KubernetesClientServicesListSupplier.class); + + private CoreV1Api coreV1Api; + + private KubernetesClientProperties kubernetesClientProperties; + + public KubernetesClientServicesListSupplier(Environment environment, KubernetesServiceInstanceMapper mapper, + KubernetesDiscoveryProperties discoveryProperties, CoreV1Api coreV1Api, + KubernetesClientProperties kubernetesClientProperties) { + super(environment, mapper, discoveryProperties); + this.coreV1Api = coreV1Api; + this.kubernetesClientProperties = kubernetesClientProperties; + } + + @Override + public Flux> get() { + LOG.info("Getting services with id " + this.getServiceId()); + List result = new ArrayList<>(); + List services = null; + try { + if (discoveryProperties.isAllNamespaces()) { + services = coreV1Api.listServiceForAllNamespaces(null, null, "metadata.name=" + this.getServiceId(), + null, null, null, null, null, null).getItems(); + } + else { + services = coreV1Api.listNamespacedService(kubernetesClientProperties.getNamespace(), null, null, null, + "metadata.name=" + this.getServiceId(), null, null, null, null, null).getItems(); + } + services.forEach(service -> result.add(mapper.map(service))); + } + catch (ApiException e) { + LOG.warn("Error retrieving service with name " + this.getServiceId(), e); + } + LOG.info("Returning services: " + result); + return Flux.defer(() -> Flux.just(result)); + } + +} diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/main/resources/META-INF/spring.factories b/spring-cloud-kubernetes-client-loadbalancer/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..870a69d7 --- /dev/null +++ b/spring-cloud-kubernetes-client-loadbalancer/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.kubernetes.client.loadbalancer.KubernetesClientLoadBalancerAutoConfiguration diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerPodModeTests.java b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerPodModeTests.java new file mode 100644 index 00000000..f0a88d03 --- /dev/null +++ b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerPodModeTests.java @@ -0,0 +1,124 @@ +/* + * 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.loadbalancer; + +import java.io.IOException; +import java.util.Collections; + +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.util.ClientBuilder; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.cloud.client.loadbalancer.LoadBalancerRequest; +import org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerDiscoveryClient; +import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.mock.http.client.MockClientHttpResponse; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author Ryan Baxter + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = KubernetesClientLoadBalancerPodModeTests.App.class, + properties = { "spring.cloud.kubernetes.client.namespace=default" }) +public class KubernetesClientLoadBalancerPodModeTests { + + @Autowired + private TestRestTemplate rest; + + @Test + public void testLoadBalancer() { + ResponseEntity map = rest.getForEntity("/servicea", String.class); + assertThat(map.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @RestController + @SpringBootApplication + static class App { + + public static void main(String[] args) { + SpringApplication.run(App.class, args); + } + + @Bean + public ApiClient apiClient() { + return new ClientBuilder().build(); + } + + @Bean + public BlockingLoadBalancerClient blockingLoadBalancerClient() { + BlockingLoadBalancerClient client = mock(BlockingLoadBalancerClient.class); + try { + ClientHttpResponse response = new MockClientHttpResponse("hello".getBytes(), HttpStatus.OK); + when(client.execute(eq("servicea-wiremock"), any(LoadBalancerRequest.class))).thenReturn(response); + when(client.execute(eq("servicea-wiremock"), any(ServiceInstance.class), + any(LoadBalancerRequest.class))).thenReturn(response); + } + catch (IOException e) { + e.printStackTrace(); + } + return client; + } + + @Bean + public KubernetesInformerDiscoveryClient kubernetesInformerDiscoveryClient() { + KubernetesInformerDiscoveryClient client = mock(KubernetesInformerDiscoveryClient.class); + ServiceInstance instance = new DefaultServiceInstance("servicea-wiremock1", "servicea-wiremock", "fake", + 8888, false); + given(client.getInstances(eq("servicea-wiremock"))).willReturn(Collections.singletonList(instance)); + return client; + } + + @Bean + @LoadBalanced + RestTemplate restTemplate() { + return new RestTemplateBuilder().build(); + } + + @GetMapping("/servicea") + public String greeting() { + return restTemplate().getForObject("http://servicea-wiremock", String.class); + } + + } + +} diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerServiceModeTests.java b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerServiceModeTests.java new file mode 100644 index 00000000..11774bb3 --- /dev/null +++ b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerServiceModeTests.java @@ -0,0 +1,157 @@ +/* + * 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.loadbalancer; + +import java.io.IOException; + +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.openapi.models.V1ServiceBuilder; +import io.kubernetes.client.openapi.models.V1ServiceList; +import io.kubernetes.client.openapi.models.V1ServiceListBuilder; +import io.kubernetes.client.openapi.models.V1ServicePortBuilder; +import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder; +import io.kubernetes.client.util.ClientBuilder; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.cloud.client.loadbalancer.LoadBalancerRequest; +import org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerDiscoveryClient; +import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.mock.http.client.MockClientHttpResponse; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author Ryan Baxter + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = KubernetesClientLoadBalancerServiceModeTests.App.class, + properties = { "spring.cloud.kubernetes.loadbalancer.mode=SERVICE", + "spring.cloud.kubernetes.client.namespace=default" }) +public class KubernetesClientLoadBalancerServiceModeTests { + + private static final V1ServiceList SERVICE_LIST = new V1ServiceListBuilder() + .addToItems( + new V1ServiceBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("servicea-wiremock") + .withNamespace("default").withResourceVersion("1").addToLabels("beta", "true") + .addToAnnotations("org.springframework.cloud", "true").withUid("0").build()) + .withSpec(new V1ServiceSpecBuilder().withClusterIP("10.96.0.1").withSessionAffinity("None") + .withType("ClusterIP") + .addToPorts(new V1ServicePortBuilder().withPort(80).withName("http") + .withProtocol("TCP").withNewTargetPort(8080).build()) + .build()) + .build()) + .build(); + + @Autowired + private TestRestTemplate rest; + + @Test + public void testLoadBalancer() { + ResponseEntity map = rest.getForEntity("/servicea", String.class); + assertThat(map.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @RestController + @SpringBootApplication + static class App { + + public static void main(String[] args) { + SpringApplication.run(App.class, args); + } + + @Bean + public ApiClient apiClient() { + return new ClientBuilder().build(); + } + + @Bean + public CoreV1Api coreV1Api() { + CoreV1Api coreV1Api = mock(CoreV1Api.class); + try { + when(coreV1Api.listNamespacedService(eq("default"), eq(null), eq(null), eq(null), + eq("metadata.name=servicea-wiremock"), eq(null), eq(null), eq(null), eq(null), eq(null))) + .thenReturn(SERVICE_LIST); + } + catch (ApiException e) { + e.printStackTrace(); + } + return coreV1Api; + } + + @Bean + public BlockingLoadBalancerClient blockingLoadBalancerClient() { + BlockingLoadBalancerClient client = mock(BlockingLoadBalancerClient.class); + try { + ClientHttpResponse response = new MockClientHttpResponse("hello".getBytes(), HttpStatus.OK); + when(client.execute(eq("servicea-wiremock"), any(LoadBalancerRequest.class))).thenReturn(response); + when(client.execute(eq("servicea-wiremock"), any(ServiceInstance.class), + any(LoadBalancerRequest.class))).thenReturn(response); + } + catch (IOException e) { + e.printStackTrace(); + } + return client; + } + + @Bean + public KubernetesInformerDiscoveryClient kubernetesInformerDiscoveryClient() { + // Mock this so the real implementation does not try to connect to the K8S API + // Server + KubernetesInformerDiscoveryClient client = mock(KubernetesInformerDiscoveryClient.class); + return client; + } + + @Bean + @LoadBalanced + RestTemplate restTemplate() { + return new RestTemplateBuilder().build(); + } + + @GetMapping("/servicea") + public String greeting() { + return restTemplate().getForObject("http://servicea-wiremock", String.class); + } + + } + +} diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientServiceInstanceMapperTests.java b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientServiceInstanceMapperTests.java new file mode 100644 index 00000000..c79c9c20 --- /dev/null +++ b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientServiceInstanceMapperTests.java @@ -0,0 +1,87 @@ +/* + * 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.loadbalancer; + +import java.util.HashMap; +import java.util.Map; + +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceBuilder; +import io.kubernetes.client.openapi.models.V1ServicePortBuilder; +import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesServiceInstance; +import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesLoadBalancerProperties; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Ryan Baxter + */ +class KubernetesClientServiceInstanceMapperTests { + + @Test + void basicMap() { + KubernetesLoadBalancerProperties loadBalancerProperties = new KubernetesLoadBalancerProperties(); + KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(); + KubernetesClientServiceInstanceMapper mapper = new KubernetesClientServiceInstanceMapper(loadBalancerProperties, + kubernetesDiscoveryProperties); + + V1Service service = new V1ServiceBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("database").withUid("0").withResourceVersion("0") + .withNamespace("default").addToAnnotations("org.springframework.cloud", "true") + .addToLabels("beta", "true").build()) + .withSpec(new V1ServiceSpecBuilder() + .addToPorts(new V1ServicePortBuilder().withPort(80).withName("http").build()).build()) + .build(); + + KubernetesServiceInstance serviceInstance = mapper.map(service); + Map metadata = new HashMap<>(); + metadata.put("org.springframework.cloud", "true"); + metadata.put("beta", "true"); + KubernetesServiceInstance result = new KubernetesServiceInstance("0", "database", + "database.default.svc.cluster.local", 80, metadata, false); + assertThat(serviceInstance).isEqualTo(result); + } + + @Test + void multiportMap() { + KubernetesLoadBalancerProperties loadBalancerProperties = new KubernetesLoadBalancerProperties(); + loadBalancerProperties.setPortName("https"); + KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(); + KubernetesClientServiceInstanceMapper mapper = new KubernetesClientServiceInstanceMapper(loadBalancerProperties, + kubernetesDiscoveryProperties); + + V1Service service = new V1ServiceBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("database").withUid("0").withResourceVersion("0") + .withNamespace("default").build()) + .withSpec(new V1ServiceSpecBuilder() + .addToPorts(new V1ServicePortBuilder().withPort(80).withName("http").build(), + new V1ServicePortBuilder().withPort(443).withName("https").build()) + .build()) + .build(); + + KubernetesServiceInstance serviceInstance = mapper.map(service); + KubernetesServiceInstance result = new KubernetesServiceInstance("0", "database", + "database.default.svc.cluster.local", 443, new HashMap(), true); + assertThat(serviceInstance).isEqualTo(result); + } + +} diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientServicesListSupplierTests.java b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientServicesListSupplierTests.java new file mode 100644 index 00000000..8be52830 --- /dev/null +++ b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientServicesListSupplierTests.java @@ -0,0 +1,173 @@ +/* + * 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.loadbalancer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.Configuration; +import io.kubernetes.client.openapi.JSON; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.openapi.models.V1ServiceBuilder; +import io.kubernetes.client.openapi.models.V1ServiceList; +import io.kubernetes.client.openapi.models.V1ServicePortBuilder; +import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder; +import io.kubernetes.client.util.ClientBuilder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesServiceInstance; +import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesLoadBalancerProperties; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; +import org.springframework.mock.env.MockEnvironment; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; + +/** + * @author Ryan Baxter + */ +class KubernetesClientServicesListSupplierTests { + + private static final V1ServiceList SERVICE_LIST = new V1ServiceList().addItemsItem(new V1ServiceBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("service1").withNamespace("default") + .withResourceVersion("1").addToLabels("beta", "true") + .addToAnnotations("org.springframework.cloud", "true").withUid("0").build()) + .withSpec(new V1ServiceSpecBuilder() + .addToPorts(new V1ServicePortBuilder().withPort(80).withName("http").build()).build()) + .build()); + + private static final V1ServiceList SERVICE_LIST_ALL_NAMESPACE = new V1ServiceList() + .addItemsItem(new V1ServiceBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("service1").withNamespace("default") + .withResourceVersion("1").addToLabels("beta", "true") + .addToAnnotations("org.springframework.cloud", "true").withUid("0").build()) + .withSpec(new V1ServiceSpecBuilder() + .addToPorts(new V1ServicePortBuilder().withPort(80).withName("http").build()).build()) + .build()) + .addItemsItem(new V1ServiceBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("service1").withNamespace("test") + .withResourceVersion("1").withUid("1").build()) + .withSpec(new V1ServiceSpecBuilder() + .addToPorts(new V1ServicePortBuilder().withPort(80).withName("http").build(), + new V1ServicePortBuilder().withPort(443).withName("https").build()) + .build()) + .build()); + + private static WireMockServer wireMockServer; + + @BeforeAll + public static void setup() { + wireMockServer = new WireMockServer(options().dynamicPort()); + + wireMockServer.start(); + WireMock.configureFor("localhost", wireMockServer.port()); + + ApiClient client = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build(); + client.setDebugging(true); + Configuration.setDefaultApiClient(client); + } + + @AfterAll + public static void after() { + wireMockServer.stop(); + } + + @AfterEach + public void afterEach() { + WireMock.reset(); + } + + @Test + void getList() { + MockEnvironment env = new MockEnvironment(); + env.setProperty(LoadBalancerClientFactory.PROPERTY_NAME, "service1"); + KubernetesClientProperties kubernetesClientProperties = new KubernetesClientProperties(); + kubernetesClientProperties.setNamespace("default"); + KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(); + CoreV1Api coreV1Api = new CoreV1Api(); + KubernetesClientServiceInstanceMapper mapper = new KubernetesClientServiceInstanceMapper( + new KubernetesLoadBalancerProperties(), kubernetesDiscoveryProperties); + KubernetesClientServicesListSupplier listSupplier = new KubernetesClientServicesListSupplier(env, mapper, + kubernetesDiscoveryProperties, coreV1Api, kubernetesClientProperties); + + stubFor(get(urlMatching("^/api/v1/namespaces/default/services.*")) + .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(SERVICE_LIST)))); + + Flux> instances = listSupplier.get(); + + Map metadata = new HashMap<>(); + metadata.put("org.springframework.cloud", "true"); + metadata.put("beta", "true"); + KubernetesServiceInstance service1 = new KubernetesServiceInstance("0", "service1", + "service1.default.svc.cluster.local", 80, metadata, false); + List services = new ArrayList<>(); + services.add(service1); + + StepVerifier.create(instances).expectNext(services).verifyComplete(); + } + + @Test + void getListAllNamespaces() { + MockEnvironment env = new MockEnvironment(); + env.setProperty(LoadBalancerClientFactory.PROPERTY_NAME, "service1"); + KubernetesClientProperties kubernetesClientProperties = new KubernetesClientProperties(); + kubernetesClientProperties.setNamespace("default"); + KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(); + kubernetesDiscoveryProperties.setAllNamespaces(true); + CoreV1Api coreV1Api = new CoreV1Api(); + KubernetesClientServiceInstanceMapper mapper = new KubernetesClientServiceInstanceMapper( + new KubernetesLoadBalancerProperties(), kubernetesDiscoveryProperties); + KubernetesClientServicesListSupplier listSupplier = new KubernetesClientServicesListSupplier(env, mapper, + kubernetesDiscoveryProperties, coreV1Api, kubernetesClientProperties); + + stubFor(get(urlMatching("^/api/v1/services.*")) + .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(SERVICE_LIST_ALL_NAMESPACE)))); + + Flux> instances = listSupplier.get(); + + Map metadata = new HashMap<>(); + metadata.put("org.springframework.cloud", "true"); + metadata.put("beta", "true"); + KubernetesServiceInstance service1 = new KubernetesServiceInstance("0", "service1", + "service1.default.svc.cluster.local", 80, metadata, false); + KubernetesServiceInstance service2 = new KubernetesServiceInstance("1", "service1", + "service1.test.svc.cluster.local", 80, new HashMap<>(), false); + List services = new ArrayList<>(); + services.add(service1); + services.add(service2); + + StepVerifier.create(instances).expectNext(services).verifyComplete(); + } + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesServiceInstance.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesServiceInstance.java index 042c9995..085c0ad7 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesServiceInstance.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesServiceInstance.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2019 the original author or authors. + * 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. @@ -132,6 +132,13 @@ public class KubernetesServiceInstance implements ServiceInstance { && Objects.equals(metadata, that.metadata); } + @Override + public String toString() { + return "KubernetesServiceInstance{" + "instanceId='" + instanceId + '\'' + ", serviceId='" + serviceId + '\'' + + ", host='" + host + '\'' + ", port=" + port + ", uri=" + uri + ", secure=" + secure + ", metadata=" + + metadata + '}'; + } + @Override public int hashCode() { return Objects.hash(instanceId, serviceId, host, port, uri, secure, metadata); diff --git a/spring-cloud-kubernetes-dependencies/pom.xml b/spring-cloud-kubernetes-dependencies/pom.xml index 43f3339b..d44d8bff 100644 --- a/spring-cloud-kubernetes-dependencies/pom.xml +++ b/spring-cloud-kubernetes-dependencies/pom.xml @@ -40,6 +40,7 @@ 1.5.5 0.1.2 3.14.4 + 2.26.3 @@ -133,6 +134,13 @@ spring-cloud-kubernetes-fabric8-loadbalancer ${project.version} + + + org.springframework.cloud + spring-cloud-kubernetes-client-loadbalancer + ${project.version} + + org.springframework.cloud spring-cloud-kubernetes-test-support @@ -170,6 +178,12 @@ ${project.version} + + org.springframework.cloud + spring-cloud-starter-kubernetes-client-loadbalancer + ${project.version} + + org.springframework.cloud spring-cloud-starter-kubernetes-fabric8-all @@ -259,6 +273,13 @@ + + com.github.tomakehurst + wiremock-jre8 + ${wiremock.version} + test + + com.squareup.okhttp3 okhttp diff --git a/spring-cloud-kubernetes-integration-tests/run.sh b/spring-cloud-kubernetes-integration-tests/run.sh index af6772ae..43817390 100755 --- a/spring-cloud-kubernetes-integration-tests/run.sh +++ b/spring-cloud-kubernetes-integration-tests/run.sh @@ -24,6 +24,7 @@ ALL_INTEGRATION_PROJECTS=( "spring-cloud-kubernetes-core-k8s-client-it" "spring-cloud-kubernetes-client-config-it" "spring-cloud-kubernetes-configuration-watcher-it" + "spring-cloud-kubernetes-client-loadbalancer-it" ) INTEGRATION_PROJECTS=(${INTEGRATION_PROJECTS:-${ALL_INTEGRATION_PROJECTS[@]}}) diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/k8s/deployment-it.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/k8s/deployment-it.yaml new file mode 100644 index 00000000..f1f90791 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/k8s/deployment-it.yaml @@ -0,0 +1,26 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + creationTimestamp: null + labels: + app: spring-cloud-kubernetes-client-loadbalancer-it + name: spring-cloud-kubernetes-client-loadbalancer-it-deployment +spec: + replicas: 1 + selector: + matchLabels: + app: spring-cloud-kubernetes-client-loadbalancer-it + strategy: {} + template: + metadata: + creationTimestamp: null + labels: + app: spring-cloud-kubernetes-client-loadbalancer-it + spec: + serviceAccountName: spring-cloud-kubernetes-serviceaccount + containers: + - image: springcloud/spring-cloud-kubernetes-client-loadbalancer-it:2.0.0-SNAPSHOT + imagePullPolicy: IfNotPresent + name: spring-cloud-kubernetes-client-loadbalancer-it + resources: {} +status: {} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/k8s/service-it.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/k8s/service-it.yaml new file mode 100644 index 00000000..52b7cd81 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/k8s/service-it.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + creationTimestamp: null + labels: + app: spring-cloud-kubernetes-client-loadbalancer-it + name: spring-cloud-kubernetes-client-loadbalancer-it +spec: + ports: + - name: 80-8080 + port: 80 + protocol: TCP + targetPort: 8080 + selector: + app: spring-cloud-kubernetes-client-loadbalancer-it + type: ClusterIP +status: + loadBalancer: {} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/pom.xml new file mode 100644 index 00000000..da75d7f5 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/pom.xml @@ -0,0 +1,90 @@ + + + + org.springframework.cloud + spring-cloud-kubernetes-integration-tests + 2.0.0-SNAPSHOT + + 4.0.0 + + spring-cloud-kubernetes-client-loadbalancer-it + + + + + org.springframework.cloud + spring-cloud-starter-kubernetes-client-loadbalancer + + + org.springframework.cloud + spring-cloud-kubernetes-test-support + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + io.kubernetes + client-java + + + io.kubernetes + client-java-extended + + + com.github.docker-java + docker-java-core + test + + + com.github.docker-java + docker-java-transport-httpclient5 + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + ${env.IMAGE} + + build-image + + + + package + + build-image + + + + + + + + + + imagename + + + !env.IMAGE + + + + springcloud/${project.artifactId}:${project.version} + + + + + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/skaffold.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/skaffold.yaml new file mode 100644 index 00000000..b8a58d22 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/skaffold.yaml @@ -0,0 +1,19 @@ +apiVersion: skaffold/v2alpha3 +kind: Config +metadata: + name: spring-cloud-kubernetes-client-loadbalancer-it +build: + artifacts: + - image: springcloud/spring-cloud-kubernetes-client-loadbalancer-it + custom: + buildCommand: "../../mvnw clean install" + dependencies: + paths: + - src + - pom.xml +deploy: + kubectl: + manifests: + - k8s/deployment-it.yaml + - k8s/service-it.yaml + - ../permissions.yaml diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/KubernetesClientLoadBalancerApplicationIt.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/KubernetesClientLoadBalancerApplicationIt.java new file mode 100644 index 00000000..a806fee8 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/main/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/KubernetesClientLoadBalancerApplicationIt.java @@ -0,0 +1,64 @@ +/* + * 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.loadbalancer.it; + +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.context.annotation.Bean; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +/** + * @author Ryan Baxter + */ + +@SpringBootApplication +@RestController +public class KubernetesClientLoadBalancerApplicationIt { + + @Autowired + DiscoveryClient discoveryClient; + + public static void main(String[] args) { + SpringApplication.run(KubernetesClientLoadBalancerApplicationIt.class, args); + } + + @Bean + @LoadBalanced + RestTemplate restTemplate() { + return new RestTemplateBuilder().build(); + } + + @GetMapping("/servicea") + public Map greeting() { + return restTemplate().getForObject("http://servicea-wiremock/__admin/mappings", Map.class); + } + + @GetMapping("/services") + public List services() { + return discoveryClient.getServices(); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/main/resources/application.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/main/resources/application.yaml new file mode 100644 index 00000000..98b2266e --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/main/resources/application.yaml @@ -0,0 +1 @@ +debug: true diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/LoadBalancerIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/LoadBalancerIT.java new file mode 100644 index 00000000..a7a5ff44 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/LoadBalancerIT.java @@ -0,0 +1,227 @@ +/* + * 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.loadbalancer.it; + +import java.io.IOException; +import java.time.Duration; +import java.util.Map; + +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.apis.AppsV1Api; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.apis.NetworkingV1beta1Api; +import io.kubernetes.client.openapi.models.NetworkingV1beta1Ingress; +import io.kubernetes.client.openapi.models.V1Deployment; +import io.kubernetes.client.openapi.models.V1Service; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.cloud.kubernetes.integration.tests.commons.K8SUtils; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.web.client.ResponseErrorHandler; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.springframework.cloud.kubernetes.integration.tests.commons.K8SUtils.createApiClient; + +/** + * @author Ryan Baxter + */ +public class LoadBalancerIT { + + private static final Log LOG = LogFactory.getLog(LoadBalancerIT.class); + + private static final String WIREMOCK_DEPLOYMENT_NAME = "servicea-wiremock-deployment"; + + private static final String WIREMOCK_APP_NAME = "servicea-wiremock"; + + private static final String SPRING_CLOUD_K8S_LOADBALANCER_DEPLOYMENT_NAME = "spring-cloud-kubernetes-client-loadbalancer-it-deployment"; + + private static final String SPRING_CLOUD_K8S_LOADBALANCER_APP_NAME = "spring-cloud-kubernetes-client-loadbalancer-it"; + + private static final String WIREMOCK_HOST = "localhost"; + + private static final String WIREMOCK_PATH = "/wiremock"; + + private static final int WIREMOCK_PORT = 80; + + private static final String NAMESPACE = "default"; + + private ApiClient client; + + private CoreV1Api api; + + private AppsV1Api appsApi; + + private NetworkingV1beta1Api networkingApi; + + private K8SUtils k8SUtils; + + @Before + public void setup() throws Exception { + this.client = createApiClient(); + this.api = new CoreV1Api(); + this.appsApi = new AppsV1Api(); + this.networkingApi = new NetworkingV1beta1Api(); + this.k8SUtils = new K8SUtils(api, appsApi); + + deployWiremock(); + + // Check to make sure the wiremock deployment is ready + k8SUtils.waitForDeployment(WIREMOCK_DEPLOYMENT_NAME, NAMESPACE); + + // Check to see if endpoint is ready + k8SUtils.waitForEndpointReady(WIREMOCK_APP_NAME, NAMESPACE); + + } + + @Test + public void testLoadBalancerServiceMode() throws Exception { + try { + deployLoadbalancerServiceIt(); + testLoadBalancer(); + } + finally { + cleanup(); + } + } + + @Test + public void testLoadBalancerPodMode() throws Exception { + try { + deployLoadbalancerPodIt(); + testLoadBalancer(); + } + finally { + cleanup(); + } + } + + private void cleanup() throws ApiException { + appsApi.deleteCollectionNamespacedDeployment(NAMESPACE, null, null, null, + "metadata.name=" + SPRING_CLOUD_K8S_LOADBALANCER_DEPLOYMENT_NAME, null, null, null, null, null, null, + null, null); + api.deleteNamespacedService(SPRING_CLOUD_K8S_LOADBALANCER_APP_NAME, NAMESPACE, null, null, null, null, null, + null); + networkingApi.deleteNamespacedIngress("it-ingress", NAMESPACE, null, null, null, null, null, null); + } + + private void testLoadBalancer() throws Exception { + // Check to make sure the controller deployment is ready + k8SUtils.waitForDeployment(SPRING_CLOUD_K8S_LOADBALANCER_DEPLOYMENT_NAME, NAMESPACE); + RestTemplate rest = new RestTemplateBuilder().build(); + rest.setErrorHandler(new ResponseErrorHandler() { + @Override + public boolean hasError(ClientHttpResponse clientHttpResponse) throws IOException { + LOG.warn("Received response status code: " + clientHttpResponse.getRawStatusCode()); + if (clientHttpResponse.getRawStatusCode() == 503) { + return false; + } + return true; + } + + @Override + public void handleError(ClientHttpResponse clientHttpResponse) throws IOException { + + } + }); + // Sometimes the NGINX ingress takes a bit to catch up and realize the service is + // available and we get a 503, we just need to wait a bit + await().timeout(Duration.ofSeconds(60)) + .until(() -> rest.getForEntity("http://localhost:80/loadbalancer-it/servicea", String.class) + .getStatusCode().is2xxSuccessful()); + Map result = rest.getForObject("http://localhost:80/loadbalancer-it/servicea", Map.class); + assertThat(result.containsKey("mappings")).isTrue(); + assertThat(result.containsKey("meta")).isTrue(); + + } + + @After + public void after() throws Exception { + appsApi.deleteCollectionNamespacedDeployment(NAMESPACE, null, null, null, + "metadata.name=" + WIREMOCK_DEPLOYMENT_NAME, null, null, null, null, null, null, null, null); + + api.deleteNamespacedService(WIREMOCK_APP_NAME, NAMESPACE, null, null, null, null, null, null); + networkingApi.deleteNamespacedIngress("wiremock-ingress", NAMESPACE, null, null, null, null, null, null); + + } + + private void deployLoadbalancerServiceIt() throws Exception { + appsApi.createNamespacedDeployment(NAMESPACE, getLoadbalancerServiceItDeployment(), null, null, null); + api.createNamespacedService(NAMESPACE, getLoadbalancerItService(), null, null, null); + networkingApi.createNamespacedIngress(NAMESPACE, getLoadbalancerItIngress(), null, null, null); + } + + private void deployLoadbalancerPodIt() throws Exception { + appsApi.createNamespacedDeployment(NAMESPACE, getLoadbalancerPodItDeployment(), null, null, null); + api.createNamespacedService(NAMESPACE, getLoadbalancerItService(), null, null, null); + networkingApi.createNamespacedIngress(NAMESPACE, getLoadbalancerItIngress(), null, null, null); + } + + private V1Service getLoadbalancerItService() throws Exception { + V1Service service = (V1Service) k8SUtils + .readYamlFromClasspath("spring-cloud-kubernetes-client-loadbalancer-it-service.yaml"); + return service; + } + + private V1Deployment getLoadbalancerServiceItDeployment() throws Exception { + V1Deployment deployment = (V1Deployment) k8SUtils + .readYamlFromClasspath("spring-cloud-kubernetes-client-loadbalancer-service-it-deployment.yaml"); + return deployment; + } + + private V1Deployment getLoadbalancerPodItDeployment() throws Exception { + V1Deployment deployment = (V1Deployment) k8SUtils + .readYamlFromClasspath("spring-cloud-kubernetes-client-loadbalancer-service-it-deployment.yaml"); + return deployment; + } + + private NetworkingV1beta1Ingress getLoadbalancerItIngress() throws Exception { + NetworkingV1beta1Ingress ingress = (NetworkingV1beta1Ingress) k8SUtils + .readYamlFromClasspath("spring-cloud-kubernetes-client-loadbalancer-it-ingress.yaml"); + return ingress; + } + + private void deployWiremock() throws Exception { + appsApi.createNamespacedDeployment(NAMESPACE, getWireockDeployment(), null, null, null); + api.createNamespacedService(NAMESPACE, getWiremockAppService(), null, null, null); + networkingApi.createNamespacedIngress(NAMESPACE, getWiremockIngress(), null, null, null); + } + + private NetworkingV1beta1Ingress getWiremockIngress() throws Exception { + NetworkingV1beta1Ingress ingress = (NetworkingV1beta1Ingress) k8SUtils + .readYamlFromClasspath("wiremock-ingress.yaml"); + return ingress; + } + + private V1Service getWiremockAppService() throws Exception { + V1Service service = (V1Service) k8SUtils.readYamlFromClasspath("wiremock-service.yaml"); + return service; + } + + private V1Deployment getWireockDeployment() throws Exception { + V1Deployment deployment = (V1Deployment) k8SUtils.readYamlFromClasspath("wiremock-deployment.yaml"); + return deployment; + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/spring-cloud-kubernetes-client-loadbalancer-it-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/spring-cloud-kubernetes-client-loadbalancer-it-ingress.yaml new file mode 100644 index 00000000..ecd5239f --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/spring-cloud-kubernetes-client-loadbalancer-it-ingress.yaml @@ -0,0 +1,14 @@ +apiVersion: networking.k8s.io/v1beta1 +kind: Ingress +metadata: + name: it-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: /$2 +spec: + rules: + - http: + paths: + - path: /loadbalancer-it(/|$)(.*) + backend: + serviceName: spring-cloud-kubernetes-client-loadbalancer-it + servicePort: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/spring-cloud-kubernetes-client-loadbalancer-it-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/spring-cloud-kubernetes-client-loadbalancer-it-service.yaml new file mode 100644 index 00000000..a2ee8e36 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/spring-cloud-kubernetes-client-loadbalancer-it-service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: spring-cloud-kubernetes-client-loadbalancer-it + name: spring-cloud-kubernetes-client-loadbalancer-it +spec: + ports: + - name: http + port: 8080 + targetPort: 8080 + selector: + app: spring-cloud-kubernetes-client-loadbalancer-it + type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/spring-cloud-kubernetes-client-loadbalancer-pod-it-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/spring-cloud-kubernetes-client-loadbalancer-pod-it-deployment.yaml new file mode 100644 index 00000000..55518cfe --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/spring-cloud-kubernetes-client-loadbalancer-pod-it-deployment.yaml @@ -0,0 +1,31 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: spring-cloud-kubernetes-client-loadbalancer-it-deployment +spec: + selector: + matchLabels: + app: spring-cloud-kubernetes-client-loadbalancer-it + template: + metadata: + labels: + app: spring-cloud-kubernetes-client-loadbalancer-it + spec: + serviceAccountName: spring-cloud-kubernetes-serviceaccount + containers: + - name: spring-cloud-kubernetes-client-loadbalancer-it + env: + - name: SPRING_CLOUD_KUBERNETES_LOADBALANCER_MODE + value: POD + image: docker.io/springcloud/spring-cloud-kubernetes-client-loadbalancer-it:2.0.0-SNAPSHOT + imagePullPolicy: IfNotPresent + readinessProbe: + httpGet: + port: 8080 + path: /actuator/health/readiness + livenessProbe: + httpGet: + port: 8080 + path: /actuator/health/liveness + ports: + - containerPort: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/spring-cloud-kubernetes-client-loadbalancer-service-it-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/spring-cloud-kubernetes-client-loadbalancer-service-it-deployment.yaml new file mode 100644 index 00000000..27b9d6bc --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/spring-cloud-kubernetes-client-loadbalancer-service-it-deployment.yaml @@ -0,0 +1,31 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: spring-cloud-kubernetes-client-loadbalancer-it-deployment +spec: + selector: + matchLabels: + app: spring-cloud-kubernetes-client-loadbalancer-it + template: + metadata: + labels: + app: spring-cloud-kubernetes-client-loadbalancer-it + spec: + serviceAccountName: spring-cloud-kubernetes-serviceaccount + containers: + - name: spring-cloud-kubernetes-client-loadbalancer-it + env: + - name: SPRING_CLOUD_KUBERNETES_LOADBALANCER_MODE + value: SERVICE + image: docker.io/springcloud/spring-cloud-kubernetes-client-loadbalancer-it:2.0.0-SNAPSHOT + imagePullPolicy: IfNotPresent + readinessProbe: + httpGet: + port: 8080 + path: /actuator/health/readiness + livenessProbe: + httpGet: + port: 8080 + path: /actuator/health/liveness + ports: + - containerPort: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/wiremock-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/wiremock-deployment.yaml new file mode 100644 index 00000000..8243368c --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/wiremock-deployment.yaml @@ -0,0 +1,27 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: servicea-wiremock-deployment +spec: + selector: + matchLabels: + app: servicea-wiremock + template: + metadata: + labels: + app: servicea-wiremock + spec: + containers: + - name: servicea-wiremock + image: rodolpheche/wiremock + imagePullPolicy: IfNotPresent + readinessProbe: + httpGet: + port: 8080 + path: /__admin/mappings + livenessProbe: + httpGet: + port: 8080 + path: /__admin/mappings + ports: + - containerPort: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/wiremock-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/wiremock-ingress.yaml new file mode 100644 index 00000000..9c2ab4be --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/wiremock-ingress.yaml @@ -0,0 +1,14 @@ +apiVersion: networking.k8s.io/v1beta1 +kind: Ingress +metadata: + name: wiremock-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: /$2 +spec: + rules: + - http: + paths: + - path: /wiremock(/|$)(.*) + backend: + serviceName: servicea-wiremock + servicePort: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/wiremock-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/wiremock-service.yaml new file mode 100644 index 00000000..ef30d58a --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-loadbalancer-it/src/test/resources/wiremock-service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: servicea-wiremock + name: servicea-wiremock +spec: + ports: + - name: http + port: 8080 + targetPort: 8080 + selector: + app: servicea-wiremock + type: ClusterIP diff --git a/spring-cloud-starter-kubernetes-client-loadbalancer/pom.xml b/spring-cloud-starter-kubernetes-client-loadbalancer/pom.xml new file mode 100644 index 00000000..38d2bd56 --- /dev/null +++ b/spring-cloud-starter-kubernetes-client-loadbalancer/pom.xml @@ -0,0 +1,30 @@ + + + + spring-cloud-kubernetes + org.springframework.cloud + 2.0.0-SNAPSHOT + + 4.0.0 + + spring-cloud-starter-kubernetes-client-loadbalancer + + + + org.springframework.cloud + spring-cloud-kubernetes-client-autoconfig + + + org.springframework.cloud + spring-cloud-kubernetes-client-loadbalancer + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + + +