diff --git a/README.adoc b/README.adoc index 132c003f..96b1230c 100644 --- a/README.adoc +++ b/README.adoc @@ -27,8 +27,7 @@ This reference guide covers how to use Spring Cloud Kubernetes. == Why do you need Spring Cloud Kubernetes? -Spring Cloud Kubernetes provide Spring Cloud common interface implementations that consume Kubernetes native services. -The main objective of the projects provided in this repository is to facilitate the integration of Spring Cloud and Spring Boot applications running inside Kubernetes. +Spring Cloud Kubernetes provides implementations of well known Spring Cloud interfaces allowing developers to build and run Spring Cloud applications on Kubernetes. While this project may be useful to you when building a cloud native application, it is also not a requirement in order to deploy a Spring Boot app on Kubernetes. If you are just getting started in your journey to running your Spring Boot app on Kubernetes you can accomplish a lot with nothing more than a basic Spring Boot app and Kubernetes itself. To learn more, you can get started by reading the https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#cloud-deployment-kubernetes[Spring Boot reference documentation for deploying to Kubernetes ] and also working through the workshop material https://hackmd.io/@ryanjbaxter/spring-on-k8s-workshop[Spring and Kubernetes]. == Starters diff --git a/docs/pom.xml b/docs/pom.xml index 95dd87f4..0d5b9010 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -8,7 +8,7 @@ org.springframework.cloud spring-cloud-kubernetes - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT pom Spring Cloud Kubernetes Docs diff --git a/docs/src/main/asciidoc/info-contributor.adoc b/docs/src/main/asciidoc/info-contributor.adoc new file mode 100644 index 00000000..073dfdca --- /dev/null +++ b/docs/src/main/asciidoc/info-contributor.adoc @@ -0,0 +1,7 @@ +== Info Contributor + +Spring Cloud Kubernetes includes an `InfoContributor` which adds Pod information to +Spring Boot's `/info` Acturator endpoint. + +You can disable this `InfoContributor` by setting `management.info.kubernetes.enabled` +to `false` in `bootstrap.[properties | yaml]`. diff --git a/docs/src/main/asciidoc/load-balancer.adoc b/docs/src/main/asciidoc/load-balancer.adoc new file mode 100644 index 00000000..bdb4813c --- /dev/null +++ b/docs/src/main/asciidoc/load-balancer.adoc @@ -0,0 +1,28 @@ +== LoadBalancer for Kubernetes +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. +==== +[source,xml] +---- + + org.springframework.cloud + spring-cloud-starter-kubernetes-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] +---- +spring.cloud.kubernetes.loadbalancer.mode=SERVICE +---- +==== + +To enabled load balancing across all namespaces use the following property. Property from `spring-cloud-kubernetes-discovery` module is respected. +==== +[source] +---- +spring.cloud.kubernetes.discovery.all-namespaces=true +---- +==== diff --git a/docs/src/main/asciidoc/property-source-config.adoc b/docs/src/main/asciidoc/property-source-config.adoc index d3435424..9d3d102e 100644 --- a/docs/src/main/asciidoc/property-source-config.adoc +++ b/docs/src/main/asciidoc/property-source-config.adoc @@ -279,6 +279,10 @@ You can specify multiple (exact) file paths in `spring.cloud.kubernetes.config.p NOTE: You have to provide the full exact path to each property file, because directories are not being recursively parsed. +NOTE: If you use `spring.cloud.kubernetes.config.paths` or `spring.cloud.kubernetes.secrets.path` the automatic reload +functionality will not work. You will need to make a `POST` request to the `/actuator/refresh` endpoint or +restart/redeploy the application. + .Properties: [options="header,footer"] |=== diff --git a/docs/src/main/asciidoc/spring-cloud-kubernetes.adoc b/docs/src/main/asciidoc/spring-cloud-kubernetes.adoc index 3ad907fa..1d627d1d 100644 --- a/docs/src/main/asciidoc/spring-cloud-kubernetes.adoc +++ b/docs/src/main/asciidoc/spring-cloud-kubernetes.adoc @@ -5,8 +5,7 @@ This reference guide covers how to use Spring Cloud Kubernetes. == Why do you need Spring Cloud Kubernetes? -Spring Cloud Kubernetes provide Spring Cloud common interface implementations that consume Kubernetes native services. -The main objective of the projects provided in this repository is to facilitate the integration of Spring Cloud and Spring Boot applications running inside Kubernetes. +Spring Cloud Kubernetes provides implementations of well known Spring Cloud interfaces allowing developers to build and run Spring Cloud applications on Kubernetes. While this project may be useful to you when building a cloud native application, it is also not a requirement in order to deploy a Spring Boot app on Kubernetes. If you are just getting started in your journey to running your Spring Boot app on Kubernetes you can accomplish a lot with nothing more than a basic Spring Boot app and Kubernetes itself. To learn more, you can get started by reading the https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#cloud-deployment-kubernetes[Spring Boot reference documentation for deploying to Kubernetes ] and also working through the workshop material https://hackmd.io/@ryanjbaxter/spring-on-k8s-workshop[Spring and Kubernetes]. include::getting-started.adoc[] @@ -22,8 +21,12 @@ include::kubernetes-awareness.adoc[] include::pod-health-indicator.adoc[] +include::info-contributor.adoc[] + include::leader-election.adoc[] +include::load-balancer.adoc[] + include::security-service-accounts.adoc[] include::service-registry.adoc[] diff --git a/pom.xml b/pom.xml index 6be73261..63c83338 100644 --- a/pom.xml +++ b/pom.xml @@ -24,13 +24,13 @@ org.springframework.cloud spring-cloud-build - 2.3.1.BUILD-SNAPSHOT + 2.3.1.RELEASE org.springframework.cloud spring-cloud-kubernetes - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT pom Spring Cloud Kubernetes @@ -63,9 +63,9 @@ - 2.2.4.BUILD-SNAPSHOT - 2.2.4.BUILD-SNAPSHOT - 2.2.4.BUILD-SNAPSHOT + 2.2.7.BUILD-SNAPSHOT + 2.2.7.BUILD-SNAPSHOT + 2.2.7.BUILD-SNAPSHOT 3.5 @@ -99,6 +99,8 @@ spring-cloud-kubernetes-istio spring-cloud-kubernetes-integration-tests docs + spring-cloud-kubernetes-loadbalancer + spring-cloud-starter-kubernetes-loadbalancer diff --git a/spring-cloud-kubernetes-config/pom.xml b/spring-cloud-kubernetes-config/pom.xml index cc9a7782..7bc60865 100644 --- a/spring-cloud-kubernetes-config/pom.xml +++ b/spring-cloud-kubernetes-config/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/ConfigMapPropertySourceLocator.java b/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/ConfigMapPropertySourceLocator.java index 39116614..beb5caf0 100644 --- a/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/ConfigMapPropertySourceLocator.java +++ b/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/ConfigMapPropertySourceLocator.java @@ -113,7 +113,7 @@ public class ConfigMapPropertySourceLocator implements PropertySourceLocator { }).filter(Files::isRegularFile).forEach(p -> { try { String content = new String(Files.readAllBytes(p)).trim(); - String filename = p.getFileName().toString().toLowerCase(); + String filename = p.toAbsolutePath().toString().toLowerCase(); if (filename.endsWith(".properties")) { addPropertySourceIfNeeded( c -> PROPERTIES_TO_MAP diff --git a/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/reload/ConfigReloadAutoConfiguration.java b/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/reload/ConfigReloadAutoConfiguration.java index a676dae8..0dae6bc7 100644 --- a/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/reload/ConfigReloadAutoConfiguration.java +++ b/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/reload/ConfigReloadAutoConfiguration.java @@ -24,6 +24,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -36,6 +38,7 @@ import org.springframework.cloud.kubernetes.config.ConfigMapPropertySourceLocato import org.springframework.cloud.kubernetes.config.SecretsPropertySourceLocator; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.AbstractEnvironment; import org.springframework.scheduling.annotation.EnableAsync; @@ -72,32 +75,28 @@ public class ConfigReloadAutoConfiguration { @Autowired private KubernetesClient kubernetesClient; - @Autowired - private ConfigMapPropertySourceLocator configMapPropertySourceLocator; - - @Autowired - private SecretsPropertySourceLocator secretsPropertySourceLocator; - /** * @param properties config reload properties * @param strategy configuration update strategy * @return a bean that listen to configuration changes and fire a reload. */ @Bean - @ConditionalOnMissingBean + @Conditional(OnConfigEnabledOrSecretsEnabled.class) public ConfigurationChangeDetector propertyChangeWatcher( - ConfigReloadProperties properties, ConfigurationUpdateStrategy strategy) { + ConfigReloadProperties properties, ConfigurationUpdateStrategy strategy, + @Autowired( + required = false) ConfigMapPropertySourceLocator configMapPropertySourceLocator, + @Autowired( + required = false) SecretsPropertySourceLocator secretsPropertySourceLocator) { switch (properties.getMode()) { case POLLING: return new PollingConfigurationChangeDetector(this.environment, properties, this.kubernetesClient, strategy, - this.configMapPropertySourceLocator, - this.secretsPropertySourceLocator); + configMapPropertySourceLocator, secretsPropertySourceLocator); case EVENT: return new EventBasedConfigurationChangeDetector(this.environment, properties, this.kubernetesClient, strategy, - this.configMapPropertySourceLocator, - this.secretsPropertySourceLocator); + configMapPropertySourceLocator, secretsPropertySourceLocator); } throw new IllegalStateException( "Unsupported configuration reload mode: " + properties.getMode()); @@ -148,6 +147,24 @@ public class ConfigReloadAutoConfiguration { } } + private static class OnConfigEnabledOrSecretsEnabled extends AnyNestedCondition { + + OnConfigEnabledOrSecretsEnabled() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnBean(ConfigMapPropertySourceLocator.class) + static class configEnabled { + + } + + @ConditionalOnBean(SecretsPropertySourceLocator.class) + static class secretsEnabled { + + } + + } + } } diff --git a/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/reload/EventBasedConfigurationChangeDetector.java b/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/reload/EventBasedConfigurationChangeDetector.java index 32a9d463..6d7c0eba 100644 --- a/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/reload/EventBasedConfigurationChangeDetector.java +++ b/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/reload/EventBasedConfigurationChangeDetector.java @@ -66,7 +66,8 @@ public class EventBasedConfigurationChangeDetector extends ConfigurationChangeDe public void watch() { boolean activated = false; - if (this.properties.isMonitoringConfigMaps()) { + if (this.properties.isMonitoringConfigMaps() + && this.configMapPropertySourceLocator != null) { try { String name = "config-maps-watch"; this.watches.put(name, this.kubernetesClient.configMaps() @@ -91,7 +92,8 @@ public class EventBasedConfigurationChangeDetector extends ConfigurationChangeDe } } - if (this.properties.isMonitoringSecrets()) { + if (this.properties.isMonitoringSecrets() + && this.secretsPropertySourceLocator != null) { try { activated = false; String name = "secrets-watch"; diff --git a/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/reload/PollingConfigurationChangeDetector.java b/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/reload/PollingConfigurationChangeDetector.java index d315ef4c..5cef72a5 100644 --- a/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/reload/PollingConfigurationChangeDetector.java +++ b/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/reload/PollingConfigurationChangeDetector.java @@ -68,7 +68,8 @@ public class PollingConfigurationChangeDetector extends ConfigurationChangeDetec public void executeCycle() { boolean changedConfigMap = false; - if (this.properties.isMonitoringConfigMaps()) { + if (this.properties.isMonitoringConfigMaps() + && this.configMapPropertySourceLocator != null) { List currentConfigMapSources = findPropertySources( ConfigMapPropertySource.class); @@ -81,7 +82,8 @@ public class PollingConfigurationChangeDetector extends ConfigurationChangeDetec } boolean changedSecrets = false; - if (this.properties.isMonitoringSecrets()) { + if (this.properties.isMonitoringSecrets() + && this.secretsPropertySourceLocator != null) { List currentSecretSources = locateMapPropertySources( this.secretsPropertySourceLocator, this.environment); if (currentSecretSources != null && !currentSecretSources.isEmpty()) { diff --git a/spring-cloud-kubernetes-config/src/test/java/org/springframework/cloud/kubernetes/config/ConfigMapsFromFilePathsTests.java b/spring-cloud-kubernetes-config/src/test/java/org/springframework/cloud/kubernetes/config/ConfigMapsFromFilePathsTests.java index 363e6425..03912687 100644 --- a/spring-cloud-kubernetes-config/src/test/java/org/springframework/cloud/kubernetes/config/ConfigMapsFromFilePathsTests.java +++ b/spring-cloud-kubernetes-config/src/test/java/org/springframework/cloud/kubernetes/config/ConfigMapsFromFilePathsTests.java @@ -45,11 +45,14 @@ import static org.springframework.cloud.kubernetes.config.ConfigMapTestUtil.crea "spring.cloud.kubernetes.config.enableApi=false", "spring.cloud.kubernetes.config.paths=" + ConfigMapsFromFilePathsTests.FIRST_FILE_NAME_FULL_PATH + "," - + ConfigMapsFromFilePathsTests.SECOND_FILE_NAME_FULL_PATH }) + + ConfigMapsFromFilePathsTests.SECOND_FILE_NAME_FULL_PATH + "," + + ConfigMapsFromFilePathsTests.FIRST_FILE_NAME_DUPLICATED_FULL_PATH }) public class ConfigMapsFromFilePathsTests { protected static final String FILES_ROOT_PATH = "/tmp/scktests"; + protected static final String FILES_SUB_PATH = "another-directory"; + protected static final String FIRST_FILE_NAME = "application.properties"; protected static final String SECOND_FILE_NAME = "extra.properties"; @@ -65,6 +68,9 @@ public class ConfigMapsFromFilePathsTests { protected static final String UNUSED_FILE_NAME_FULL_PATH = FILES_ROOT_PATH + "/" + UNUSED_FILE_NAME; + protected static final String FIRST_FILE_NAME_DUPLICATED_FULL_PATH = FILES_ROOT_PATH + + "/" + FILES_SUB_PATH + "/" + FIRST_FILE_NAME; + @ClassRule public static KubernetesServer server = new KubernetesServer(); @@ -87,12 +93,14 @@ public class ConfigMapsFromFilePathsTests { System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "test"); System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true"); - Files.createDirectories(Paths.get(FILES_ROOT_PATH)); + Files.createDirectories(Paths.get(FILES_ROOT_PATH + "/" + FILES_SUB_PATH)); createFileWithContent(FIRST_FILE_NAME_FULL_PATH, "bean.greeting=Hello from path!"); createFileWithContent(SECOND_FILE_NAME_FULL_PATH, "bean.farewell=Bye from path!"); createFileWithContent(UNUSED_FILE_NAME_FULL_PATH, "bean.morning=Morning from path!"); + createFileWithContent(FIRST_FILE_NAME_DUPLICATED_FULL_PATH, + "bean.bonjour=Bonjour from path!"); } @AfterClass @@ -125,4 +133,10 @@ public class ConfigMapsFromFilePathsTests { .expectBody().jsonPath("content").isEqualTo("Good morning, World!"); } + @Test + public void bonjourInputShouldReturnPropertyFromDuplicatedFile() { + this.webClient.get().uri("/api/bonjour").exchange().expectStatus().isOk() + .expectBody().jsonPath("content").isEqualTo("Bonjour from path!"); + } + } diff --git a/spring-cloud-kubernetes-config/src/test/java/org/springframework/cloud/kubernetes/config/example/GreetingController.java b/spring-cloud-kubernetes-config/src/test/java/org/springframework/cloud/kubernetes/config/example/GreetingController.java index d3410409..1b2983d7 100644 --- a/spring-cloud-kubernetes-config/src/test/java/org/springframework/cloud/kubernetes/config/example/GreetingController.java +++ b/spring-cloud-kubernetes-config/src/test/java/org/springframework/cloud/kubernetes/config/example/GreetingController.java @@ -52,4 +52,10 @@ public class GreetingController { return new ResponseMessage(String.format(this.properties.getMorning(), name)); } + @RequestMapping("/api/bonjour") + public ResponseMessage bonjour( + @RequestParam(value = "name", defaultValue = "World") String name) { + return new ResponseMessage(String.format(this.properties.getBonjour(), name)); + } + } diff --git a/spring-cloud-kubernetes-config/src/test/java/org/springframework/cloud/kubernetes/config/example/GreetingProperties.java b/spring-cloud-kubernetes-config/src/test/java/org/springframework/cloud/kubernetes/config/example/GreetingProperties.java index e8c613ee..0c47e8dc 100644 --- a/spring-cloud-kubernetes-config/src/test/java/org/springframework/cloud/kubernetes/config/example/GreetingProperties.java +++ b/spring-cloud-kubernetes-config/src/test/java/org/springframework/cloud/kubernetes/config/example/GreetingProperties.java @@ -27,6 +27,8 @@ public class GreetingProperties { private String morning = "Good morning, %s!"; + private String bonjour = "Bonjour, %s!"; + public String getGreeting() { return this.greeting; } @@ -51,4 +53,12 @@ public class GreetingProperties { this.morning = morning; } + public String getBonjour() { + return this.bonjour; + } + + public void setBonjour(String bonjour) { + this.bonjour = bonjour; + } + } diff --git a/spring-cloud-kubernetes-core/pom.xml b/spring-cloud-kubernetes-core/pom.xml index de5e73fb..3b47b7e7 100644 --- a/spring-cloud-kubernetes-core/pom.xml +++ b/spring-cloud-kubernetes-core/pom.xml @@ -22,7 +22,7 @@ org.springframework.cloud spring-cloud-kubernetes - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-core/src/main/java/org/springframework/cloud/kubernetes/KubernetesAutoConfiguration.java b/spring-cloud-kubernetes-core/src/main/java/org/springframework/cloud/kubernetes/KubernetesAutoConfiguration.java index b9dd95c6..034a8e14 100644 --- a/spring-cloud-kubernetes-core/src/main/java/org/springframework/cloud/kubernetes/KubernetesAutoConfiguration.java +++ b/spring-cloud-kubernetes-core/src/main/java/org/springframework/cloud/kubernetes/KubernetesAutoConfiguration.java @@ -26,6 +26,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; +import org.springframework.boot.actuate.autoconfigure.info.ConditionalOnEnabledInfoContributor; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -130,7 +131,7 @@ public class KubernetesAutoConfiguration { base.getHttpsProxy())) .withProxyUsername(or(kubernetesClientProperties.getProxyUsername(), base.getProxyUsername())) - .withPassword(or(kubernetesClientProperties.getProxyPassword(), + .withProxyPassword(or(kubernetesClientProperties.getProxyPassword(), base.getProxyPassword())) .withNoProxy( or(kubernetesClientProperties.getNoProxy(), base.getNoProxy())) @@ -166,6 +167,7 @@ public class KubernetesAutoConfiguration { } @Bean + @ConditionalOnEnabledInfoContributor("kubernetes") public KubernetesInfoContributor kubernetesInfoContributor(PodUtils podUtils) { return new KubernetesInfoContributor(podUtils); } diff --git a/spring-cloud-kubernetes-core/src/test/java/org/springframework/cloud/kubernetes/KubernetesAutoConfigurationTests.java b/spring-cloud-kubernetes-core/src/test/java/org/springframework/cloud/kubernetes/KubernetesAutoConfigurationTests.java new file mode 100644 index 00000000..6edd151a --- /dev/null +++ b/spring-cloud-kubernetes-core/src/test/java/org/springframework/cloud/kubernetes/KubernetesAutoConfigurationTests.java @@ -0,0 +1,78 @@ +/* + * 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; + +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.server.mock.KubernetesServer; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.kubernetes.example.App; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = App.class, + properties = { "spring.cloud.kubernetes.client.password=mypassword", + "spring.cloud.kubernetes.client.proxy-password=myproxypassword" }) +public class KubernetesAutoConfigurationTests { + + @ClassRule + public static KubernetesServer server = new KubernetesServer(); + + @Autowired + ConfigurableApplicationContext context; + + @BeforeClass + public static void setUpBeforeClass() { + KubernetesClient mockClient = server.getClient(); + + // Configure the kubernetes master url to point to the mock server + System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, + mockClient.getConfiguration().getMasterUrl()); + System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true"); + System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, + "false"); + System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "test"); + System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true"); + } + + @Test + public void beansAreCreated() { + assertThat(context.getBeanNamesForType(Config.class)).hasSize(1); + assertThat(context.getBeanNamesForType(KubernetesClient.class)).hasSize(1); + assertThat(context.getBeanNamesForType(StandardPodUtils.class)).hasSize(1); + assertThat(context.getBeanNamesForType(KubernetesHealthIndicator.class)) + .hasSize(1); + assertThat(context.getBeanNamesForType(KubernetesInfoContributor.class)) + .hasSize(1); + + Config config = context.getBean(Config.class); + assertThat(config.getPassword()).isEqualTo("mypassword"); + assertThat(config.getProxyPassword()).isEqualTo("myproxypassword"); + } + +} diff --git a/spring-cloud-kubernetes-dependencies/pom.xml b/spring-cloud-kubernetes-dependencies/pom.xml index 1642c08d..c27479dd 100644 --- a/spring-cloud-kubernetes-dependencies/pom.xml +++ b/spring-cloud-kubernetes-dependencies/pom.xml @@ -23,21 +23,22 @@ spring-cloud-dependencies-parent org.springframework.cloud - 2.3.1.BUILD-SNAPSHOT + 2.3.2.BUILD-SNAPSHOT spring-cloud-kubernetes-dependencies - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT pom Spring Cloud Kubernetes :: Dependencies Spring Cloud Kubernetes Dependencies 1.4.0.Final 1.15.2 + 0.13.0 4.10.3 1.0.0 0.1.2 - 3.12.0 + 3.14.4 @@ -98,6 +99,12 @@ ${project.version} + + org.springframework.cloud + spring-cloud-kubernetes-loadbalancer + ${project.version} + + org.springframework.cloud @@ -117,6 +124,12 @@ ${project.version} + + org.springframework.cloud + spring-cloud-starter-kubernetes-loadbalancer + ${project.version} + + org.springframework.cloud spring-cloud-starter-kubernetes-all @@ -148,6 +161,18 @@ ${arquillian-cube.version} test + + io.specto + hoverfly-java-junit5 + ${hoverfly.version} + test + + + io.specto + hoverfly-java + ${hoverfly.version} + test + io.fabric8 diff --git a/spring-cloud-kubernetes-discovery/pom.xml b/spring-cloud-kubernetes-discovery/pom.xml index 9d77049e..9f11a66f 100644 --- a/spring-cloud-kubernetes-discovery/pom.xml +++ b/spring-cloud-kubernetes-discovery/pom.xml @@ -22,7 +22,7 @@ org.springframework.cloud spring-cloud-kubernetes - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClient.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClient.java index ac8eb75d..3479a602 100644 --- a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClient.java +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClient.java @@ -41,6 +41,7 @@ import org.springframework.util.Assert; import org.springframework.util.StringUtils; import static java.util.stream.Collectors.toMap; +import static org.springframework.cloud.kubernetes.discovery.KubernetesServiceInstance.NAMESPACE_METADATA_KEY; /** * Kubeneretes implementation of {@link DiscoveryClient}. @@ -153,6 +154,10 @@ public class KubernetesDiscoveryClient implements DiscoveryClient { endpointMetadata.putAll(portMetadata); } + if (this.properties.isAllNamespaces()) { + endpointMetadata.put(NAMESPACE_METADATA_KEY, namespace); + } + List addresses = s.getAddresses(); for (EndpointAddress endpointAddress : addresses) { String instanceId = null; @@ -162,7 +167,8 @@ public class KubernetesDiscoveryClient implements DiscoveryClient { EndpointPort endpointPort = findEndpointPort(s); instances.add(new KubernetesServiceInstance(instanceId, serviceId, - endpointAddress, endpointPort, endpointMetadata, + endpointAddress.getIp(), endpointPort.getPort(), + endpointMetadata, this.isServicePortSecureResolver .resolve(new DefaultIsServicePortSecureResolver.Input( endpointPort.getPort(), diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesServiceInstance.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesServiceInstance.java index 9c1a6180..28043269 100644 --- a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesServiceInstance.java +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesServiceInstance.java @@ -19,9 +19,6 @@ package org.springframework.cloud.kubernetes.discovery; import java.net.URI; import java.util.Map; -import io.fabric8.kubernetes.api.model.EndpointAddress; -import io.fabric8.kubernetes.api.model.EndpointPort; - import org.springframework.cloud.client.ServiceInstance; /** @@ -31,49 +28,50 @@ 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 COLN = ":"; + private static final String COLON = ":"; private final String instanceId; private final String serviceId; - private final EndpointAddress endpointAddress; + private final String host; - private final EndpointPort endpointPort; + 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 endpointAddress the address where the service instance can be found. - * @param endpointPort the port on which the service is running. + * @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. - * @deprecated - use other constructor */ - @Deprecated - public KubernetesServiceInstance(String serviceId, EndpointAddress endpointAddress, - EndpointPort endpointPort, Map metadata, Boolean secure) { - this(null, serviceId, endpointAddress, endpointPort, metadata, secure); - } - - public KubernetesServiceInstance(String instanceId, String serviceId, - EndpointAddress endpointAddress, EndpointPort endpointPort, - Map metadata, Boolean secure) { + public KubernetesServiceInstance(String instanceId, String serviceId, String host, + int port, Map metadata, Boolean secure) { this.instanceId = instanceId; this.serviceId = serviceId; - this.endpointAddress = endpointAddress; - this.endpointPort = endpointPort; + this.host = host; + this.port = port; this.metadata = metadata; this.secure = secure; + this.uri = createUri(secure ? HTTPS_PREFIX : HTTP_PREFIX, host, port); } @Override @@ -88,12 +86,12 @@ public class KubernetesServiceInstance implements ServiceInstance { @Override public String getHost() { - return this.endpointAddress.getIp(); + return this.host; } @Override public int getPort() { - return this.endpointPort.getPort(); + return this.port; } @Override @@ -103,10 +101,7 @@ public class KubernetesServiceInstance implements ServiceInstance { @Override public URI getUri() { - StringBuilder sb = new StringBuilder(); - sb.append(getScheme()).append(COLN).append(DSL).append(getHost()).append(COLN) - .append(getPort()); - return URI.create(sb.toString()); + return uri; } public Map getMetadata() { @@ -118,4 +113,15 @@ public class KubernetesServiceInstance implements ServiceInstance { return isSecure() ? HTTPS_PREFIX : HTTP_PREFIX; } + private URI createUri(String scheme, String host, int port) { + StringBuilder sb = new StringBuilder(); + sb.append(scheme).append(COLON).append(DSL).append(host).append(COLON) + .append(port); + return URI.create(sb.toString()); + } + + public String getNamespace() { + return this.metadata != null ? this.metadata.get(NAMESPACE_METADATA_KEY) : null; + } + } diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientTest.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientTest.java index 85a77231..a70aa292 100644 --- a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientTest.java +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientTest.java @@ -377,6 +377,14 @@ public class KubernetesDiscoveryClientTest { .hasSize(1); assertThat(instances).filteredOn(s -> s.getHost().equals("ip2") && !s.isSecure()) .hasSize(1); + assertThat(instances) + .filteredOn(s -> s.getServiceId().contains("endpoint") + && ((KubernetesServiceInstance) s).getNamespace().equals("test")) + .hasSize(1); + assertThat(instances) + .filteredOn(s -> s.getServiceId().contains("endpoint") + && ((KubernetesServiceInstance) s).getNamespace().equals("test2")) + .hasSize(1); assertThat(instances).filteredOn(s -> s.getInstanceId().equals("60")).hasSize(1); assertThat(instances).filteredOn(s -> s.getInstanceId().equals("70")).hasSize(1); } diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesServiceInstanceTests.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesServiceInstanceTests.java index eb3738e5..8cf648c6 100644 --- a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesServiceInstanceTests.java +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/KubernetesServiceInstanceTests.java @@ -37,7 +37,8 @@ public class KubernetesServiceInstanceTests { EndpointPort port = new EndpointPort(); port.setPort(8080); KubernetesServiceInstance instance = new KubernetesServiceInstance("123", - "myservice", address, port, Collections.emptyMap(), secure); + "myservice", address.getIp(), port.getPort(), Collections.emptyMap(), + secure); assertThat(instance.getInstanceId()).isEqualTo("123"); assertThat(instance.getServiceId()).isEqualTo("myservice"); diff --git a/spring-cloud-kubernetes-examples/kubernetes-circuitbreaker-ribbon-example/greeting-service/pom.xml b/spring-cloud-kubernetes-examples/kubernetes-circuitbreaker-ribbon-example/greeting-service/pom.xml index 7280c346..4537f7ab 100644 --- a/spring-cloud-kubernetes-examples/kubernetes-circuitbreaker-ribbon-example/greeting-service/pom.xml +++ b/spring-cloud-kubernetes-examples/kubernetes-circuitbreaker-ribbon-example/greeting-service/pom.xml @@ -23,7 +23,7 @@ org.springframework.cloud kubernetes-circuitbreaker-ribbon-example - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT greeting-service diff --git a/spring-cloud-kubernetes-examples/kubernetes-circuitbreaker-ribbon-example/name-service/pom.xml b/spring-cloud-kubernetes-examples/kubernetes-circuitbreaker-ribbon-example/name-service/pom.xml index 53d4db60..1f65c943 100644 --- a/spring-cloud-kubernetes-examples/kubernetes-circuitbreaker-ribbon-example/name-service/pom.xml +++ b/spring-cloud-kubernetes-examples/kubernetes-circuitbreaker-ribbon-example/name-service/pom.xml @@ -23,7 +23,7 @@ org.springframework.cloud kubernetes-circuitbreaker-ribbon-example - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT name-service diff --git a/spring-cloud-kubernetes-examples/kubernetes-circuitbreaker-ribbon-example/pom.xml b/spring-cloud-kubernetes-examples/kubernetes-circuitbreaker-ribbon-example/pom.xml index 745deeec..dad8a45d 100644 --- a/spring-cloud-kubernetes-examples/kubernetes-circuitbreaker-ribbon-example/pom.xml +++ b/spring-cloud-kubernetes-examples/kubernetes-circuitbreaker-ribbon-example/pom.xml @@ -23,7 +23,7 @@ spring-cloud-kubernetes-examples org.springframework.cloud - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT org.springframework.cloud diff --git a/spring-cloud-kubernetes-examples/kubernetes-hello-world-example/pom.xml b/spring-cloud-kubernetes-examples/kubernetes-hello-world-example/pom.xml index 1fffcac5..9c3e8d4d 100644 --- a/spring-cloud-kubernetes-examples/kubernetes-hello-world-example/pom.xml +++ b/spring-cloud-kubernetes-examples/kubernetes-hello-world-example/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes-examples org.springframework.cloud - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/pom.xml b/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/pom.xml index f3372266..58f9d287 100644 --- a/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/pom.xml +++ b/spring-cloud-kubernetes-examples/kubernetes-leader-election-example/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-kubernetes-examples - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT kubernetes-leader-election-example diff --git a/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/README.md b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/README.md new file mode 100644 index 00000000..27c01a76 --- /dev/null +++ b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/README.md @@ -0,0 +1,94 @@ +## Kubernetes Spring Cloud LoadBalancer Example + +This example demonstrates how to use [Spring Cloud LoadBalancer](https://github.com/spring-cloud-incubator/spring-cloud-loadbalancer). In our example the REST `greeting service` calls the `name service`. +As the Spring Cloud LoadBalancer for Kubernetes is configured within this example, it will fetch from the Kubernetes API Server the list of the endpoints available for the name service and load-balance the request between the IP addresses available. + +### Running the example + +This project example runs on ALL the Kubernetes or OpenShift environments, but for development purposes you can use [Minikube - Kubernetes](https://kubernetes.io/docs/getting-started-guides/minikube/) tool +to install the platform locally within a virtual machine managed by VirtualBox, Xhyve or KVM, with no fuss. + +IMPORTANT: In order for this setup to work, you need to grant permissions to retrieve "pods", "services" and "enpoints" to the serviceaccont that will be used with the greeting-service. + +### Build/Deploy using Minikube + +First, create a new virtual machine provisioned with Kubernetes on your laptop using the command `minikube start`. + +Next, you can compile your project and generate the Kubernetes resources (yaml files containing the definition of the pod, deployment, build, service and route to be created) +like also to deploy the application on Kubernetes in one maven line by running: + +``` +mvn clean install fabric8:deploy -Dfabric8.generator.from=fabric8/java-jboss-openjdk8-jdk -Pkubernetes +``` + +### Call the Greeting service + +When maven has finished to compile the code but also to call the platform in order to deploy the yaml files generated and tell to the platform to start the process +to build/deploy the docker image and create the containers where the Spring Boot application will run 'greeting-service" and "name-service", you will be able to +check if the pods have been created using this command : + +``` +kc get pods +``` + +If the status of the Spring Boot pod application is `running` and ready state `1`, then you can +get the external address IP/Hostname to be used to call the service from your laptop + +``` +minikube service --url greeting-service +``` + +and then call the service using the curl client + +``` +curl https://IP_OR_HOSTNAME/greeting +``` + +to get a response as such + +``` +Hello from name-service-1-0dzb4!d +``` + +### Verify the load balancing + +First, scale the number of pods of the `name service` to 2 + +``` +kc scale --replicas=2 deployment name-service +``` + +Wait a few minutes before to issue the curl request to call the Greeting Service to let the platform to create the new pod. + +``` +kc get pods --selector=project=name-service +NAME READY STATUS RESTARTS AGE +name-service-1652024859-fsnfw 1/1 Running 0 33s +name-service-1652024859-wrzjs 1/1 Running 0 6m +``` + +If you issue the curl request to access the greeting service, you should see that the message response +contains a different id end of the message which corresponds to the name of the pod. + +``` +Hello from name-service-1-0ss0r! +``` + +As Spring Cloud LoadBalancer will question the Kubernetes API to get, base on the `name-service` name, the list of IP Addresses assigned to the service as endpoints, +you should see that you will get a response from one of the 2 pods running + +``` +kc get endpoints/name-service +NAME ENDPOINTS AGE +name-service 172.17.0.5:8080,172.17.0.6:8080 40m +``` + +Here is an example about what you will get + +``` +curl https://IP_OR_HOSTNAME/greeting +Hello from name-service-1652024859-hf3xv! +curl https://IP_OR_HOSTNAME/greeting +Hello from name-service-1652024859-426kv! +... +``` diff --git a/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/greeting-service/pom.xml b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/greeting-service/pom.xml new file mode 100644 index 00000000..ba4142a9 --- /dev/null +++ b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/greeting-service/pom.xml @@ -0,0 +1,92 @@ + + + + kubernetes-loadbalancer-example + org.springframework.cloud + 1.1.8.BUILD-SNAPSHOT + + 4.0.0 + + greeting-service + Spring Cloud LoadBalancer :: Greeting Service + Spring Cloud LoadBalancer :: Greeting Service + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + repackage + + + + + + + io.fabric8 + fabric8-maven-plugin + + + fmp + + resource + + + + + + + + + + + org.springframework.boot + spring-boot-dependencies + pom + import + ${spring-boot.version} + + + + + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.cloud + spring-cloud-starter-kubernetes-loadbalancer + ${project.version} + + + org.springframework.cloud + spring-cloud-starter-kubernetes + ${project.version} + + + com.squareup.okhttp3 + okhttp + + + + diff --git a/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/greeting-service/src/main/fabric8/route.yml b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/greeting-service/src/main/fabric8/route.yml new file mode 100644 index 00000000..d6881f9f --- /dev/null +++ b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/greeting-service/src/main/fabric8/route.yml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Route +metadata: + name: greeting-service +spec: + port: + targetPort: 8080 + to: + kind: Service + name: greeting-service diff --git a/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/greeting-service/src/main/java/org/springframework/cloud/kubernetes/examples/GreetingController.java b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/greeting-service/src/main/java/org/springframework/cloud/kubernetes/examples/GreetingController.java new file mode 100644 index 00000000..4ec2844f --- /dev/null +++ b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/greeting-service/src/main/java/org/springframework/cloud/kubernetes/examples/GreetingController.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020-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.examples; + +import reactor.core.publisher.Mono; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * Greeting service controller. + * + * @author Gytis Trikleris + * @author Olga Maciaszek-Sharma + */ +@RestController +public class GreetingController { + + private final NameService nameService; + + public GreetingController(NameService nameService) { + this.nameService = nameService; + } + + /** + * Endpoint to get a greeting. This endpoint uses a name server to get a name for the + * greeting. + * + * Request to the name service is guarded with a circuit breaker. Therefore if a name + * service is not available or is too slow to response fallback name is used. + * + * Delay parameter can me used to make name service response slower. + * @param delay Milliseconds for how long the response from name service should be + * delayed. + * @return Greeting string. + */ + @GetMapping("/greeting") + public Mono getGreeting( + @RequestParam(value = "delay", defaultValue = "0") int delay) { + return nameService.getName(delay) + .map(name -> String.format("Hello from %s!", name)); + } + +} diff --git a/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/greeting-service/src/main/java/org/springframework/cloud/kubernetes/examples/GreetingServiceApplication.java b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/greeting-service/src/main/java/org/springframework/cloud/kubernetes/examples/GreetingServiceApplication.java new file mode 100644 index 00000000..fb649080 --- /dev/null +++ b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/greeting-service/src/main/java/org/springframework/cloud/kubernetes/examples/GreetingServiceApplication.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020-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.examples; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.context.annotation.Bean; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * Entry point to the application. + * + * @author Gytis Trikleris + * @author Olga Maciaszek-Sharma + */ +@SpringBootApplication +@EnableDiscoveryClient +public class GreetingServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(GreetingServiceApplication.class, args); + } + + @LoadBalanced + @Bean + WebClient.Builder loadBalancedWebClientBuilder() { + return WebClient.builder(); + } + +} diff --git a/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/greeting-service/src/main/java/org/springframework/cloud/kubernetes/examples/NameService.java b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/greeting-service/src/main/java/org/springframework/cloud/kubernetes/examples/NameService.java new file mode 100644 index 00000000..7d479e32 --- /dev/null +++ b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/greeting-service/src/main/java/org/springframework/cloud/kubernetes/examples/NameService.java @@ -0,0 +1,45 @@ +/* + * Copyright 2020-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.examples; + +import reactor.core.publisher.Mono; + +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * Service invoking name-service via REST and guarded by Hystrix. + * + * @author Gytis Trikleris + * @author Olga Maciaszek-Sharma + */ +@Service +public class NameService { + + private final WebClient webClient; + + public NameService(WebClient.Builder webClientBuilder) { + webClient = webClientBuilder.build(); + } + + public Mono getName(int delay) { + return webClient.get() + .uri(String.format("http://name-service/name?delay=%d", delay)).retrieve() + .bodyToMono(String.class); + } + +} diff --git a/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/greeting-service/src/main/resources/application.yml b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/greeting-service/src/main/resources/application.yml new file mode 100644 index 00000000..5aaed995 --- /dev/null +++ b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/greeting-service/src/main/resources/application.yml @@ -0,0 +1,10 @@ +spring: + application: + name: greeting-service + cloud: + loadbalancer: + ribbon: + enabled: false + +server: + port: 8080 diff --git a/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/name-service/pom.xml b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/name-service/pom.xml new file mode 100644 index 00000000..12009678 --- /dev/null +++ b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/name-service/pom.xml @@ -0,0 +1,67 @@ + + + + kubernetes-loadbalancer-example + org.springframework.cloud + 1.1.8.BUILD-SNAPSHOT + + 4.0.0 + + name-service + + Spring Cloud LoadBalancer :: Name Service + Spring Cloud LoadBalancer :: Name Service + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + repackage + + + + + + + io.fabric8 + fabric8-maven-plugin + + + fmp + + resource + + + + + + + + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-actuator + + + + diff --git a/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/name-service/src/main/fabric8/route.yml b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/name-service/src/main/fabric8/route.yml new file mode 100644 index 00000000..adb5a71a --- /dev/null +++ b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/name-service/src/main/fabric8/route.yml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Route +metadata: + name: name-service +spec: + port: + targetPort: 8080 + to: + kind: Service + name: name-service diff --git a/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/name-service/src/main/java/org/springframework/cloud/kubernetes/examples/NameController.java b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/name-service/src/main/java/org/springframework/cloud/kubernetes/examples/NameController.java new file mode 100644 index 00000000..cad79b6a --- /dev/null +++ b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/name-service/src/main/java/org/springframework/cloud/kubernetes/examples/NameController.java @@ -0,0 +1,70 @@ +/* + * Copyright 2020-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.examples; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * Name service controller. + * + * @author Gytis Trikleris + * @author Olga Maciaszek-Sharma + */ +@RestController +public class NameController { + + private static final Logger LOG = LoggerFactory.getLogger(NameController.class); + + private final String hostName = System.getenv("HOSTNAME"); + + @GetMapping("/") + public String ribbonPing() { + LOG.info("Ribbon ping"); + return hostName; + } + + /** + * Endpoint to get a name with a capability to delay a response for some number of + * milliseconds. + * @param delayValue Milliseconds for how long the response should be delayed. + * @return Host name. + */ + @GetMapping("/name") + public Mono getName( + @RequestParam(value = "delay", defaultValue = "0") int delayValue) { + LOG.info(String.format("Returning a name '%s' with a delay '%d'", hostName, + delayValue)); + delay(delayValue); + return Mono.just(hostName); + } + + private void delay(int delayValue) { + try { + Thread.sleep(delayValue); + } + catch (InterruptedException e) { + e.printStackTrace(); + } + } + +} diff --git a/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/name-service/src/main/java/org/springframework/cloud/kubernetes/examples/NameServiceApplication.java b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/name-service/src/main/java/org/springframework/cloud/kubernetes/examples/NameServiceApplication.java new file mode 100644 index 00000000..98aef1b1 --- /dev/null +++ b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/name-service/src/main/java/org/springframework/cloud/kubernetes/examples/NameServiceApplication.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020-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.examples; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Entry point to the application. + * + * @author Gytis Trikleris + */ +@SpringBootApplication +public class NameServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(NameServiceApplication.class, args); + } + +} diff --git a/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/name-service/src/main/resources/application.yml b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/name-service/src/main/resources/application.yml new file mode 100644 index 00000000..b08d6019 --- /dev/null +++ b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/name-service/src/main/resources/application.yml @@ -0,0 +1,6 @@ +spring: + application: + name: name-service + +server: + port: 8080 \ No newline at end of file diff --git a/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/pom.xml b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/pom.xml new file mode 100644 index 00000000..b081fe4e --- /dev/null +++ b/spring-cloud-kubernetes-examples/kubernetes-loadbalancer-example/pom.xml @@ -0,0 +1,115 @@ + + + + spring-cloud-kubernetes-examples + org.springframework.cloud + 1.1.8.BUILD-SNAPSHOT + + 4.0.0 + kubernetes-loadbalancer-example + Spring Cloud LoadBalancer Example + pom + + + + kubernetes + + + + io.fabric8 + fabric8-maven-plugin + ${fabric8.maven.plugin.version} + + + fmp + + resource + build + + + + + + + + NodePort + + + + + + + + + + release + + + + io.fabric8 + fabric8-maven-plugin + ${fabric8.maven.plugin.version} + + + fmp + + resource + helm + + + + + + + + + + integration + + + + io.fabric8 + fabric8-maven-plugin + ${fabric8.maven.plugin.version} + + + fmp + + resource + build + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${maven-failsafe-plugin.version} + + + run-integration-tests + integration-test + + integration-test + verify + + + + + false + false + + + + + + + + + name-service + greeting-service + + + diff --git a/spring-cloud-kubernetes-examples/kubernetes-reload-example/pom.xml b/spring-cloud-kubernetes-examples/kubernetes-reload-example/pom.xml index ebddce11..3028107e 100644 --- a/spring-cloud-kubernetes-examples/kubernetes-reload-example/pom.xml +++ b/spring-cloud-kubernetes-examples/kubernetes-reload-example/pom.xml @@ -22,7 +22,7 @@ spring-cloud-kubernetes-examples org.springframework.cloud - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-examples/kubernetes-zipkin-example/pom.xml b/spring-cloud-kubernetes-examples/kubernetes-zipkin-example/pom.xml index 7c65abba..c05771b4 100644 --- a/spring-cloud-kubernetes-examples/kubernetes-zipkin-example/pom.xml +++ b/spring-cloud-kubernetes-examples/kubernetes-zipkin-example/pom.xml @@ -22,7 +22,7 @@ spring-cloud-kubernetes-examples org.springframework.cloud - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-examples/pom.xml b/spring-cloud-kubernetes-examples/pom.xml index d65691b6..3e45d3a2 100644 --- a/spring-cloud-kubernetes-examples/pom.xml +++ b/spring-cloud-kubernetes-examples/pom.xml @@ -23,7 +23,7 @@ spring-cloud-kubernetes org.springframework.cloud - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT spring-cloud-kubernetes-examples @@ -37,6 +37,7 @@ kubernetes-reload-example kubernetes-hello-world-example kubernetes-leader-election-example + kubernetes-loadbalancer-example diff --git a/spring-cloud-kubernetes-integration-tests/discovery/discovery-client/pom.xml b/spring-cloud-kubernetes-integration-tests/discovery/discovery-client/pom.xml index 30844baa..3b55d0a9 100644 --- a/spring-cloud-kubernetes-integration-tests/discovery/discovery-client/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/discovery/discovery-client/pom.xml @@ -7,7 +7,7 @@ org.springframework.cloud discovery-parent - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT discovery-client diff --git a/spring-cloud-kubernetes-integration-tests/discovery/discovery-service-a/pom.xml b/spring-cloud-kubernetes-integration-tests/discovery/discovery-service-a/pom.xml index de68db9a..8977e849 100644 --- a/spring-cloud-kubernetes-integration-tests/discovery/discovery-service-a/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/discovery/discovery-service-a/pom.xml @@ -7,7 +7,7 @@ org.springframework.cloud discovery-parent - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT discovery-service-a diff --git a/spring-cloud-kubernetes-integration-tests/discovery/discovery-service-b/pom.xml b/spring-cloud-kubernetes-integration-tests/discovery/discovery-service-b/pom.xml index d148a7df..3b7a42cf 100644 --- a/spring-cloud-kubernetes-integration-tests/discovery/discovery-service-b/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/discovery/discovery-service-b/pom.xml @@ -7,7 +7,7 @@ org.springframework.cloud discovery-parent - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT discovery-service-b diff --git a/spring-cloud-kubernetes-integration-tests/discovery/pom.xml b/spring-cloud-kubernetes-integration-tests/discovery/pom.xml index c7c43ade..07cb2a68 100644 --- a/spring-cloud-kubernetes-integration-tests/discovery/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/discovery/pom.xml @@ -7,7 +7,7 @@ org.springframework.cloud spring-cloud-kubernetes-integration-tests - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT Spring Cloud Kubernetes :: Integration Tests :: Discovery Parent diff --git a/spring-cloud-kubernetes-integration-tests/discovery/tests/pom.xml b/spring-cloud-kubernetes-integration-tests/discovery/tests/pom.xml index 8cb06f53..f1605404 100644 --- a/spring-cloud-kubernetes-integration-tests/discovery/tests/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/discovery/tests/pom.xml @@ -7,13 +7,21 @@ org.springframework.cloud discovery-parent - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT tests Spring Cloud Kubernetes :: Integration Tests :: Discovery Tests + + com.squareup.okhttp3 + okhttp + + + com.squareup.okhttp3 + logging-interceptor + org.springframework.boot spring-boot-starter-test diff --git a/spring-cloud-kubernetes-integration-tests/istio/pom.xml b/spring-cloud-kubernetes-integration-tests/istio/pom.xml index 407832c8..63054da3 100644 --- a/spring-cloud-kubernetes-integration-tests/istio/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/istio/pom.xml @@ -7,7 +7,7 @@ org.springframework.cloud spring-cloud-kubernetes-integration-tests - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT Spring Cloud Kubernetes :: Integration Tests :: Istio diff --git a/spring-cloud-kubernetes-integration-tests/load-balancer/pom.xml b/spring-cloud-kubernetes-integration-tests/load-balancer/pom.xml new file mode 100644 index 00000000..39d94fc8 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/load-balancer/pom.xml @@ -0,0 +1,46 @@ + + + + spring-cloud-kubernetes-integration-tests + org.springframework.cloud + 1.1.8.BUILD-SNAPSHOT + + 4.0.0 + + Spring Cloud Kubernetes :: Integration Tests :: Load Balancer + load-balancer + + + + org.springframework.cloud + spring-cloud-starter-kubernetes-loadbalancer + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + io.fabric8 + kubernetes-server-mock + test + + + io.specto + hoverfly-java-junit5 + test + + + io.specto + hoverfly-java + test + + + + diff --git a/spring-cloud-kubernetes-integration-tests/load-balancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/SimpleLoadBalancerApp.java b/spring-cloud-kubernetes-integration-tests/load-balancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/SimpleLoadBalancerApp.java new file mode 100644 index 00000000..0f91abd6 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/load-balancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/SimpleLoadBalancerApp.java @@ -0,0 +1,47 @@ +/* + * 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.loadbalancer; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.client.RestTemplateBuilder; +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; + +@SpringBootApplication +@RestController +public class SimpleLoadBalancerApp { + + public static void main(String[] args) { + SpringApplication.run(SimpleLoadBalancerApp.class, args); + } + + @Bean + @LoadBalanced + RestTemplate restTemplate() { + return new RestTemplateBuilder().build(); + } + + @GetMapping("/greeting") + public String greeting() { + return "greeting"; + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/load-balancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/LoadBalancerAllNamespacesTests.java b/spring-cloud-kubernetes-integration-tests/load-balancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/LoadBalancerAllNamespacesTests.java new file mode 100644 index 00000000..29e87c79 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/load-balancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/LoadBalancerAllNamespacesTests.java @@ -0,0 +1,81 @@ +/* + * 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.loadbalancer; + +import io.fabric8.kubernetes.api.model.ServicePortBuilder; +import io.fabric8.kubernetes.api.model.ServiceSpecBuilder; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.test.context.TestPropertySource; +import org.springframework.web.client.RestTemplate; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestPropertySource(properties = "spring.cloud.kubernetes.discovery.all-namespaces=true") +@EnableKubernetesMockClient(crud = true) +class LoadBalancerAllNamespacesTests { + + @Autowired + RestTemplate restTemplate; + + @LocalServerPort + int randomServerPort; + + static KubernetesClient client; + + @BeforeAll + static void setup() { + System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, + client.getConfiguration().getMasterUrl()); + System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true"); + System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, + "false"); + System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true"); + System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "test"); + } + + @Test + void testLoadBalancerDifferentNamespace() { + createTestData("service-b", "b"); + String response = restTemplate.getForObject("http://service-b/greeting", + String.class); + Assertions.assertNotNull(response); + Assertions.assertEquals("greeting", response); + } + + private void createTestData(String name, String namespace) { + client.services().inNamespace(namespace).createNew().withNewMetadata() + .withName(name).withNamespace(namespace).endMetadata() + .withSpec(new ServiceSpecBuilder().withPorts(new ServicePortBuilder() + .withProtocol("TCP").withPort(randomServerPort).build()).build()) + .done(); + client.endpoints().inNamespace(namespace).createNew().withNewMetadata() + .withName("service-a").withNamespace(namespace).endMetadata() + .addNewSubset().addNewAddress().withIp("localhost").endAddress() + .addNewPort().withName("http").withPort(randomServerPort).endPort() + .endSubset().done(); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/load-balancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/LoadBalancerTests.java b/spring-cloud-kubernetes-integration-tests/load-balancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/LoadBalancerTests.java new file mode 100644 index 00000000..41b01f68 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/load-balancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/LoadBalancerTests.java @@ -0,0 +1,85 @@ +/* + * 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.loadbalancer; + +import io.fabric8.kubernetes.api.model.ServicePortBuilder; +import io.fabric8.kubernetes.api.model.ServiceSpecBuilder; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.web.client.RestTemplate; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EnableKubernetesMockClient(crud = true) +class LoadBalancerTests { + + @Autowired + RestTemplate restTemplate; + + @LocalServerPort + int randomServerPort; + + static KubernetesClient client; + + @BeforeAll + static void setup() { + System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, + client.getConfiguration().getMasterUrl()); + System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true"); + System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, + "false"); + System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true"); + System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "test"); + } + + @Test + void testLoadBalancerSameNamespace() { + createTestData("service-a", "test"); + String response = restTemplate.getForObject("http://service-a/greeting", + String.class); + Assertions.assertNotNull(response); + Assertions.assertEquals("greeting", response); + } + + @Test + void testLoadBalancerDifferentNamespace() { + createTestData("service-b", "b"); + Assertions.assertThrows(IllegalStateException.class, () -> restTemplate + .getForObject("http://service-b/greeting", String.class)); + } + + private void createTestData(String name, String namespace) { + client.services().inNamespace(namespace).createNew().withNewMetadata() + .withName(name).endMetadata() + .withSpec(new ServiceSpecBuilder().withPorts(new ServicePortBuilder() + .withProtocol("TCP").withPort(randomServerPort).build()).build()) + .done(); + client.endpoints().inNamespace(namespace).createNew().withNewMetadata() + .withName("service-a").endMetadata().addNewSubset().addNewAddress() + .withIp("localhost").endAddress().addNewPort().withName("http") + .withPort(randomServerPort).endPort().endSubset().done(); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/load-balancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/LoadBalancerWithServiceTests.java b/spring-cloud-kubernetes-integration-tests/load-balancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/LoadBalancerWithServiceTests.java new file mode 100644 index 00000000..44a49670 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/load-balancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/LoadBalancerWithServiceTests.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.loadbalancer; + +import java.util.HashMap; + +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.specto.hoverfly.junit.core.Hoverfly; +import io.specto.hoverfly.junit5.HoverflyExtension; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.web.client.RestTemplate; + +import static io.specto.hoverfly.junit.core.SimulationSource.dsl; +import static io.specto.hoverfly.junit.dsl.HoverflyDsl.service; +import static io.specto.hoverfly.junit.dsl.HttpBodyConverter.json; +import static io.specto.hoverfly.junit.dsl.ResponseCreators.success; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestPropertySource(properties = { "spring.cloud.kubernetes.loadbalancer.mode=SERVICE", + "spring.cloud.kubernetes.loadbalancer.enabled=true" }) +@ExtendWith(HoverflyExtension.class) +class LoadBalancerWithServiceTests { + + private static final Logger LOGGER = LoggerFactory + .getLogger(LoadBalancerWithServiceTests.class); + + @Autowired + RestTemplate restTemplate; + + @Autowired + KubernetesClient client; + + @BeforeAll + static void setup() { + System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true"); + System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, + "false"); + System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true"); + System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "test"); + } + + @Test + void testLoadBalancerInServiceMode(Hoverfly hoverfly) { + LOGGER.info("Master URL: {}", client.getConfiguration().getMasterUrl()); + hoverfly.simulate( + dsl(service("http://service-a.test.svc.cluster.local:8080") + .get("/greeting").willReturn(success().body("greeting"))), + dsl(service(client.getConfiguration().getMasterUrl().replace("/", "") + .replace("https:", "")) + .get("/api/v1/namespaces/test/services/service-a") + .willReturn(success().body( + json(buildService("service-a", 8080, "test")))))); + String response = restTemplate.getForObject("http://service-a/greeting", + String.class); + Assertions.assertNotNull(response); + Assertions.assertEquals("greeting", response); + } + + private Service buildService(String name, int port, String namespace) { + return new ServiceBuilder().withNewMetadata().withName(name) + .withNamespace(namespace).withLabels(new HashMap<>()) + .withAnnotations(new HashMap<>()).endMetadata().withNewSpec().addNewPort() + .withPort(port).endPort().endSpec().build(); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/pom.xml b/spring-cloud-kubernetes-integration-tests/pom.xml index 0b1bd839..52a64b43 100644 --- a/spring-cloud-kubernetes-integration-tests/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-kubernetes - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT spring-cloud-kubernetes-integration-tests @@ -23,6 +23,7 @@ 1.18.2 1.4.0.Final 3.2.0 + 3.12.12 discovery - - - + load-balancer + diff --git a/spring-cloud-kubernetes-integration-tests/simple-configmap/pom.xml b/spring-cloud-kubernetes-integration-tests/simple-configmap/pom.xml index cfcdf934..15258d38 100644 --- a/spring-cloud-kubernetes-integration-tests/simple-configmap/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/simple-configmap/pom.xml @@ -7,7 +7,7 @@ org.springframework.cloud spring-cloud-kubernetes-integration-tests - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT Spring Cloud Kubernetes :: Integration Tests :: Simple Configmap @@ -31,6 +31,14 @@ org.springframework.boot spring-boot-actuator-autoconfigure + + com.squareup.okhttp3 + okhttp + + + com.squareup.okhttp3 + logging-interceptor + org.springframework.boot spring-boot-starter-test @@ -58,6 +66,7 @@ rest-assured test + diff --git a/spring-cloud-kubernetes-integration-tests/simple-core/pom.xml b/spring-cloud-kubernetes-integration-tests/simple-core/pom.xml index 27d30e96..98d5476b 100644 --- a/spring-cloud-kubernetes-integration-tests/simple-core/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/simple-core/pom.xml @@ -7,7 +7,7 @@ org.springframework.cloud spring-cloud-kubernetes-integration-tests - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT Spring Cloud Kubernetes :: Integration Tests :: Simple Core @@ -31,6 +31,14 @@ org.springframework.cloud spring-cloud-kubernetes-core + + com.squareup.okhttp3 + okhttp + + + com.squareup.okhttp3 + logging-interceptor + org.springframework.boot spring-boot-starter-test diff --git a/spring-cloud-kubernetes-istio/pom.xml b/spring-cloud-kubernetes-istio/pom.xml index da3f9b10..bf9d020e 100644 --- a/spring-cloud-kubernetes-istio/pom.xml +++ b/spring-cloud-kubernetes-istio/pom.xml @@ -5,7 +5,7 @@ spring-cloud-kubernetes org.springframework.cloud - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT 4.0.0 diff --git a/spring-cloud-kubernetes-leader/pom.xml b/spring-cloud-kubernetes-leader/pom.xml index f53fc16e..d715cd3d 100644 --- a/spring-cloud-kubernetes-leader/pom.xml +++ b/spring-cloud-kubernetes-leader/pom.xml @@ -22,7 +22,7 @@ org.springframework.cloud spring-cloud-kubernetes - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT spring-cloud-kubernetes-leader diff --git a/spring-cloud-kubernetes-loadbalancer/pom.xml b/spring-cloud-kubernetes-loadbalancer/pom.xml new file mode 100644 index 00000000..c3e84e35 --- /dev/null +++ b/spring-cloud-kubernetes-loadbalancer/pom.xml @@ -0,0 +1,50 @@ + + + + org.springframework.cloud + spring-cloud-kubernetes + 1.1.8.BUILD-SNAPSHOT + + 4.0.0 + + spring-cloud-kubernetes-loadbalancer + Spring Cloud Kubernetes :: Load Balancer + + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + org.springframework.cloud + spring-cloud-kubernetes-discovery + + + commons-lang + commons-lang + 2.6 + + + org.springframework.boot + spring-boot-starter-actuator + true + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter-test + test + + + io.fabric8 + kubernetes-server-mock + test + + + diff --git a/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerAutoConfiguration.java b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerAutoConfiguration.java new file mode 100644 index 00000000..df6f0eb1 --- /dev/null +++ b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerAutoConfiguration.java @@ -0,0 +1,45 @@ +/* + * 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.loadbalancer; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Kubernetes load balancer auto-configuration. + * + * @author Piotr Minkowski + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(KubernetesLoadBalancerProperties.class) +@ConditionalOnProperty(value = "spring.cloud.kubernetes.loadbalancer.enabled", + matchIfMissing = true) +@LoadBalancerClients( + defaultConfiguration = KubernetesLoadBalancerClientConfiguration.class) +public class KubernetesLoadBalancerAutoConfiguration { + + @Bean + KubernetesServiceInstanceMapper mapper(KubernetesLoadBalancerProperties properties, + KubernetesDiscoveryProperties discoveryProperties) { + return new KubernetesServiceInstanceMapper(properties, discoveryProperties); + } + +} diff --git a/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerClientConfiguration.java b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerClientConfiguration.java new file mode 100644 index 00000000..4201f4c8 --- /dev/null +++ b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerClientConfiguration.java @@ -0,0 +1,43 @@ +/* + * 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.loadbalancer; + +import io.fabric8.kubernetes.client.KubernetesClient; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; + +/** + * Kubernetes load balancer client configuration. + * + * @author Piotr Minkowski + */ +public class KubernetesLoadBalancerClientConfiguration { + + @Bean + @ConditionalOnProperty(name = "spring.cloud.kubernetes.loadbalancer.mode", + havingValue = "SERVICE") + KubernetesServicesListSupplier kubernetesServicesListSupplier(Environment environment, + KubernetesClient kubernetesClient, KubernetesServiceInstanceMapper mapper, + KubernetesDiscoveryProperties discoveryProperties) { + return new KubernetesServicesListSupplier(environment, kubernetesClient, mapper, + discoveryProperties); + } + +} diff --git a/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerMode.java b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerMode.java new file mode 100644 index 00000000..eaff0c08 --- /dev/null +++ b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerMode.java @@ -0,0 +1,35 @@ +/* + * 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.loadbalancer; + +/** + * Kubernetes load balancer mode enum. + * + * @author Piotr Minkowski + */ +public enum KubernetesLoadBalancerMode { + + /** + * using pod ip and port. + */ + POD, + /** + * using kubernetes service name and port. + */ + SERVICE + +} diff --git a/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerProperties.java b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerProperties.java new file mode 100644 index 00000000..1e9a3a15 --- /dev/null +++ b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesLoadBalancerProperties.java @@ -0,0 +1,114 @@ +/* + * 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.loadbalancer; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Kubernetes load balancer client properties. + * + * @author Piotr Minkowski + */ +@ConfigurationProperties(prefix = "spring.cloud.kubernetes.loadbalancer") +public class KubernetesLoadBalancerProperties { + + /** + * Load balancer enabled,default true. + */ + private Boolean enabled = true; + + /** + * {@link KubernetesLoadBalancerMode} setting load balancer server list with ip of pod + * or service name. default value is POD. + */ + private KubernetesLoadBalancerMode mode = KubernetesLoadBalancerMode.POD; + + /** + * cluster domain. + */ + private String clusterDomain = "cluster.local"; + + /** + * service port name. + */ + private String portName = "http"; + + /** + * Get cluster domain. + * @return the cluster domain + */ + public String getClusterDomain() { + return clusterDomain; + } + + /** + * Sets cluster domain. + * @param clusterDomain the cluster domain + */ + public void setClusterDomain(String clusterDomain) { + this.clusterDomain = clusterDomain; + } + + /** + * Gets mode. + * @return the mode + */ + public KubernetesLoadBalancerMode getMode() { + return mode; + } + + /** + * Sets mode. + * @param mode the mode + */ + public void setMode(KubernetesLoadBalancerMode mode) { + this.mode = mode; + } + + /** + * Gets enabled. + * @return the enabled + */ + public Boolean getEnabled() { + return enabled; + } + + /** + * Sets enabled. + * @param enabled the enabled + */ + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + /** + * Gets portName. + * @return portName port name + */ + public String getPortName() { + return portName; + } + + /** + * Sets portName. + * @param portName port name + */ + public void setPortName(String portName) { + this.portName = portName; + } + +} diff --git a/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServiceInstanceMapper.java b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServiceInstanceMapper.java new file mode 100644 index 00000000..f48eba99 --- /dev/null +++ b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServiceInstanceMapper.java @@ -0,0 +1,134 @@ +/* + * 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.loadbalancer; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServicePort; +import io.fabric8.kubernetes.client.utils.Utils; +import org.apache.commons.lang.StringUtils; + +import org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.discovery.KubernetesServiceInstance; + +/** + * Class for mapping Kubernetes Service object into {@link KubernetesServiceInstance}. + * + * @author Piotr Minkowski + */ +public class KubernetesServiceInstanceMapper { + + private final KubernetesLoadBalancerProperties properties; + + private final KubernetesDiscoveryProperties discoveryProperties; + + KubernetesServiceInstanceMapper(KubernetesLoadBalancerProperties properties, + KubernetesDiscoveryProperties discoveryProperties) { + this.properties = properties; + this.discoveryProperties = discoveryProperties; + } + + public KubernetesServiceInstance map(Service service) { + final ObjectMeta meta = service.getMetadata(); + final List ports = service.getSpec().getPorts(); + ServicePort port = null; + if (ports.size() == 1) { + port = ports.get(0); + } + else if (ports.size() > 1 + && Utils.isNotNullOrEmpty(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 = createHost(service); + final boolean secure = isSecure(service, port); + return new KubernetesServiceInstance(meta.getUid(), meta.getName(), host, + port.getPort(), getServiceMetadata(service), secure); + } + + private Map getServiceMetadata(Service service) { + final Map serviceMetadata = new HashMap<>(); + KubernetesDiscoveryProperties.Metadata metadataProps = this.discoveryProperties + .getMetadata(); + if (metadataProps.isAddLabels()) { + Map labelMetadata = getMapWithPrefixedKeys( + service.getMetadata().getLabels(), metadataProps.getLabelsPrefix()); + serviceMetadata.putAll(labelMetadata); + } + if (metadataProps.isAddAnnotations()) { + Map annotationMetadata = getMapWithPrefixedKeys( + service.getMetadata().getAnnotations(), + metadataProps.getAnnotationsPrefix()); + serviceMetadata.putAll(annotationMetadata); + } + + return serviceMetadata; + } + + private Map getMapWithPrefixedKeys(Map map, + String prefix) { + if (map == null) { + return new HashMap<>(); + } + if (!org.springframework.util.StringUtils.hasText(prefix)) { + return map; + } + final Map result = new HashMap<>(); + map.forEach((k, v) -> result.put(prefix + k, v)); + return result; + } + + private boolean isSecure(Service service, ServicePort port) { + if (service.getMetadata().getLabels() != null) { + final String securedLabelValue = service.getMetadata().getLabels() + .getOrDefault("secured", "false"); + if (securedLabelValue.equals("true")) { + return true; + } + } + + if (service.getMetadata().getAnnotations() != null) { + final String securedAnnotationValue = service.getMetadata().getAnnotations() + .getOrDefault("secured", "false"); + if (securedAnnotationValue.equals("true")) { + return true; + } + } + return (port.getName() != null && port.getName().endsWith("https")) + || port.getPort().toString().endsWith("443"); + } + + private String createHost(Service service) { + return String.format("%s.%s.svc.%s", service.getMetadata().getName(), + StringUtils.isNotBlank(service.getMetadata().getNamespace()) + ? service.getMetadata().getNamespace() : "default", + properties.getClusterDomain()); + } + +} diff --git a/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServicesListSupplier.java b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServicesListSupplier.java new file mode 100644 index 00000000..63447b91 --- /dev/null +++ b/spring-cloud-kubernetes-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServicesListSupplier.java @@ -0,0 +1,85 @@ +/* + * 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.loadbalancer; + +import java.util.ArrayList; +import java.util.List; + +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.client.KubernetesClient; +import org.apache.commons.lang.StringUtils; +import reactor.core.publisher.Flux; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; +import org.springframework.core.env.Environment; + +/** + * Implementation of {@link ServiceInstanceListSupplier} for load balancer in SERVICE + * mode. + * + * @author Piotr Minkowski + */ +public class KubernetesServicesListSupplier implements ServiceInstanceListSupplier { + + private final Environment environment; + + private final KubernetesClient kubernetesClient; + + private final KubernetesDiscoveryProperties discoveryProperties; + + private final KubernetesServiceInstanceMapper mapper; + + KubernetesServicesListSupplier(Environment environment, + KubernetesClient kubernetesClient, KubernetesServiceInstanceMapper mapper, + KubernetesDiscoveryProperties discoveryProperties) { + this.environment = environment; + this.kubernetesClient = kubernetesClient; + this.discoveryProperties = discoveryProperties; + this.mapper = mapper; + } + + @Override + public String getServiceId() { + return environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); + } + + @Override + public Flux> get() { + List result = new ArrayList<>(); + if (discoveryProperties.isAllNamespaces()) { + List services = this.kubernetesClient.services().inAnyNamespace() + .withField("metadata.name", this.getServiceId()).list().getItems(); + services.forEach(service -> result.add(mapper.map(service))); + } + else { + Service service = StringUtils.isNotBlank(this.kubernetesClient.getNamespace()) + ? this.kubernetesClient.services() + .inNamespace(this.kubernetesClient.getNamespace()) + .withName(this.getServiceId()).get() + : this.kubernetesClient.services().withName(this.getServiceId()) + .get(); + if (service != null) { + result.add(mapper.map(service)); + } + } + return Flux.defer(() -> Flux.just(result)); + } + +} diff --git a/spring-cloud-kubernetes-loadbalancer/src/main/resources/META-INF/spring.factories b/spring-cloud-kubernetes-loadbalancer/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..4a43c68f --- /dev/null +++ b/spring-cloud-kubernetes-loadbalancer/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.kubernetes.loadbalancer.KubernetesLoadBalancerAutoConfiguration diff --git a/spring-cloud-kubernetes-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServiceInstanceMapperTests.java b/spring-cloud-kubernetes-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServiceInstanceMapperTests.java new file mode 100644 index 00000000..4a799444 --- /dev/null +++ b/spring-cloud-kubernetes-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServiceInstanceMapperTests.java @@ -0,0 +1,130 @@ +/* + * 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.loadbalancer; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.fabric8.kubernetes.api.model.ServicePort; +import io.fabric8.kubernetes.api.model.ServicePortBuilder; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.discovery.KubernetesServiceInstance; + +class KubernetesServiceInstanceMapperTests { + + @Test + public void testMapperSimple() { + KubernetesLoadBalancerProperties properties = new KubernetesLoadBalancerProperties(); + KubernetesDiscoveryProperties discoveryProperties = new KubernetesDiscoveryProperties(); + Service service = buildService("test", "abc", 8080, null, new HashMap<>()); + KubernetesServiceInstance instance = new KubernetesServiceInstanceMapper( + properties, discoveryProperties).map(service); + Assertions.assertNotNull(instance); + Assertions.assertEquals("test", instance.getServiceId()); + Assertions.assertEquals("abc", instance.getInstanceId()); + } + + @Test + void testMapperMultiplePorts() { + KubernetesLoadBalancerProperties properties = new KubernetesLoadBalancerProperties(); + properties.setPortName("http"); + KubernetesDiscoveryProperties discoveryProperties = new KubernetesDiscoveryProperties(); + List ports = new ArrayList<>(); + ports.add(new ServicePortBuilder().withPort(8080).withName("web").build()); + ports.add(new ServicePortBuilder().withPort(9000).withName("http").build()); + Service service = buildService("test", "abc", ports, new HashMap<>()); + KubernetesServiceInstance instance = new KubernetesServiceInstanceMapper( + properties, discoveryProperties).map(service); + Assertions.assertNotNull(instance); + Assertions.assertEquals("test", instance.getServiceId()); + Assertions.assertEquals("abc", instance.getInstanceId()); + Assertions.assertEquals(9000, instance.getPort()); + } + + @Test + void testMapperSecure() { + KubernetesLoadBalancerProperties properties = new KubernetesLoadBalancerProperties(); + KubernetesDiscoveryProperties discoveryProperties = new KubernetesDiscoveryProperties(); + Service service = buildService("test", "abc", 443, null, new HashMap<>()); + KubernetesServiceInstance instance = new KubernetesServiceInstanceMapper( + properties, discoveryProperties).map(service); + Assertions.assertNotNull(instance); + Assertions.assertEquals("test", instance.getServiceId()); + Assertions.assertEquals("abc", instance.getInstanceId()); + Assertions.assertTrue(instance.isSecure()); + } + + @Test + void testMapperSecureNullLabelsAndAnnotations() { + KubernetesLoadBalancerProperties properties = new KubernetesLoadBalancerProperties(); + KubernetesDiscoveryProperties discoveryProperties = new KubernetesDiscoveryProperties(); + List ports = new ArrayList<>(); + ports.add(new ServicePortBuilder().withPort(443).build()); + Service service = buildService("test", "abc", ports, null, null); + KubernetesServiceInstance instance = new KubernetesServiceInstanceMapper( + properties, discoveryProperties).map(service); + Assertions.assertNotNull(instance); + Assertions.assertEquals("test", instance.getServiceId()); + Assertions.assertEquals("abc", instance.getInstanceId()); + Assertions.assertTrue(instance.isSecure()); + } + + @Test + void testMapperSecureWithLabels() { + KubernetesLoadBalancerProperties properties = new KubernetesLoadBalancerProperties(); + KubernetesDiscoveryProperties discoveryProperties = new KubernetesDiscoveryProperties(); + HashMap labels = new HashMap<>(); + labels.put("secured", "true"); + labels.put("label1", "123"); + Service service = buildService("test", "abc", 8080, null, labels); + KubernetesServiceInstance instance = new KubernetesServiceInstanceMapper( + properties, discoveryProperties).map(service); + Assertions.assertNotNull(instance); + Assertions.assertEquals("test", instance.getServiceId()); + Assertions.assertEquals("abc", instance.getInstanceId()); + Assertions.assertTrue(instance.isSecure()); + Assertions.assertEquals(2, instance.getMetadata().keySet().size()); + } + + private Service buildService(String name, String uid, int port, String portName, + Map labels) { + ServicePort servicePort = new ServicePortBuilder().withPort(port) + .withName(portName).build(); + return buildService(name, uid, Collections.singletonList(servicePort), labels); + } + + private Service buildService(String name, String uid, List ports, + Map labels, Map annotations) { + return new ServiceBuilder().withNewMetadata().withName(name).withNewUid(uid) + .addToLabels(labels).withAnnotations(annotations).endMetadata() + .withNewSpec().addAllToPorts(ports).endSpec().build(); + } + + private Service buildService(String name, String uid, List ports, + Map labels) { + return buildService(name, uid, ports, labels, new HashMap<>(0)); + } + +} diff --git a/spring-cloud-kubernetes-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServiceListSupplierTests.java b/spring-cloud-kubernetes-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServiceListSupplierTests.java new file mode 100644 index 00000000..9dd9c0ad --- /dev/null +++ b/spring-cloud-kubernetes-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/loadbalancer/KubernetesServiceListSupplierTests.java @@ -0,0 +1,116 @@ +/* + * 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.loadbalancer; + +import java.util.List; + +import io.fabric8.kubernetes.api.model.DoneableService; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.fabric8.kubernetes.api.model.ServiceList; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.Watch; +import io.fabric8.kubernetes.client.Watcher; +import io.fabric8.kubernetes.client.dsl.FilterWatchListMultiDeletable; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; +import io.fabric8.kubernetes.client.dsl.ServiceResource; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.discovery.KubernetesServiceInstance; +import org.springframework.core.env.Environment; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class KubernetesServiceListSupplierTests { + + @Mock + Environment environment; + + @Mock + KubernetesServiceInstanceMapper mapper; + + @Mock + KubernetesClient client; + + @Mock + MixedOperation> serviceOperation; + + @Mock + NonNamespaceOperation> namespaceOperation; + + @Mock + ServiceResource serviceResource; + + @Mock + FilterWatchListMultiDeletable> multiDeletable; + + @Test + void testPositiveMatch() { + when(environment.getProperty("loadbalancer.client.name")) + .thenReturn("test-service"); + when(mapper.map(any(Service.class))) + .thenReturn(new KubernetesServiceInstance("", "", "", 0, null, false)); + when(this.client.getNamespace()).thenReturn("test"); + when(this.client.services()).thenReturn(this.serviceOperation); + when(this.serviceOperation.inNamespace("test")).thenReturn(namespaceOperation); + when(this.namespaceOperation.withName("test-service")) + .thenReturn(this.serviceResource); + when(this.serviceResource.get()).thenReturn(buildService("test-service", 8080)); + KubernetesServicesListSupplier supplier = new KubernetesServicesListSupplier( + environment, client, mapper, new KubernetesDiscoveryProperties()); + List instances = supplier.get().blockFirst(); + assert instances != null; + Assertions.assertEquals(1, instances.size()); + } + + @Test + void testPositiveMatchAllNamespaces() { + when(environment.getProperty("loadbalancer.client.name")) + .thenReturn("test-service"); + when(mapper.map(any(Service.class))) + .thenReturn(new KubernetesServiceInstance("", "", "", 0, null, false)); + when(this.client.services()).thenReturn(this.serviceOperation); + when(this.serviceOperation.inAnyNamespace()).thenReturn(this.multiDeletable); + when(this.multiDeletable.withField("metadata.name", "test-service")) + .thenReturn(this.multiDeletable); + ServiceList serviceList = new ServiceList(); + serviceList.getItems().add(buildService("test-service", 8080)); + when(this.multiDeletable.list()).thenReturn(serviceList); + KubernetesDiscoveryProperties discoveryProperties = new KubernetesDiscoveryProperties(); + discoveryProperties.setAllNamespaces(true); + KubernetesServicesListSupplier supplier = new KubernetesServicesListSupplier( + environment, client, mapper, discoveryProperties); + List instances = supplier.get().blockFirst(); + assert instances != null; + Assertions.assertEquals(1, instances.size()); + } + + private Service buildService(String name, int port) { + return new ServiceBuilder().withNewMetadata().withName(name).endMetadata() + .withNewSpec().addNewPort().withPort(port).endPort().endSpec().build(); + } + +} diff --git a/spring-cloud-kubernetes-ribbon/pom.xml b/spring-cloud-kubernetes-ribbon/pom.xml index 0a097c58..bf38de92 100644 --- a/spring-cloud-kubernetes-ribbon/pom.xml +++ b/spring-cloud-kubernetes-ribbon/pom.xml @@ -22,7 +22,7 @@ spring-cloud-kubernetes org.springframework.cloud - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT 4.0.0 diff --git a/spring-cloud-starter-kubernetes-all/pom.xml b/spring-cloud-starter-kubernetes-all/pom.xml index 57ddcb57..d4df9b04 100644 --- a/spring-cloud-starter-kubernetes-all/pom.xml +++ b/spring-cloud-starter-kubernetes-all/pom.xml @@ -22,7 +22,7 @@ spring-cloud-kubernetes org.springframework.cloud - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT 4.0.0 @@ -51,6 +51,11 @@ spring-cloud-kubernetes-ribbon + + org.springframework.cloud + spring-cloud-kubernetes-loadbalancer + + diff --git a/spring-cloud-starter-kubernetes-config/pom.xml b/spring-cloud-starter-kubernetes-config/pom.xml index 6ffd84aa..eea1adac 100644 --- a/spring-cloud-starter-kubernetes-config/pom.xml +++ b/spring-cloud-starter-kubernetes-config/pom.xml @@ -22,7 +22,7 @@ spring-cloud-kubernetes org.springframework.cloud - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT 4.0.0 diff --git a/spring-cloud-starter-kubernetes-loadbalancer/pom.xml b/spring-cloud-starter-kubernetes-loadbalancer/pom.xml new file mode 100644 index 00000000..e040cdc9 --- /dev/null +++ b/spring-cloud-starter-kubernetes-loadbalancer/pom.xml @@ -0,0 +1,30 @@ + + + + spring-cloud-kubernetes + org.springframework.cloud + 1.1.8.BUILD-SNAPSHOT + + 4.0.0 + + spring-cloud-starter-kubernetes-loadbalancer + Spring Cloud Kubernetes :: Starter :: LoadBalancer + + + + org.springframework.cloud + spring-cloud-kubernetes-core + + + org.springframework.cloud + spring-cloud-kubernetes-loadbalancer + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + + diff --git a/spring-cloud-starter-kubernetes-ribbon/pom.xml b/spring-cloud-starter-kubernetes-ribbon/pom.xml index 5bd0d73c..7c000e80 100644 --- a/spring-cloud-starter-kubernetes-ribbon/pom.xml +++ b/spring-cloud-starter-kubernetes-ribbon/pom.xml @@ -22,7 +22,7 @@ spring-cloud-kubernetes org.springframework.cloud - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT 4.0.0 diff --git a/spring-cloud-starter-kubernetes/pom.xml b/spring-cloud-starter-kubernetes/pom.xml index cf433d43..cf8999f3 100644 --- a/spring-cloud-starter-kubernetes/pom.xml +++ b/spring-cloud-starter-kubernetes/pom.xml @@ -22,7 +22,7 @@ spring-cloud-kubernetes org.springframework.cloud - 1.1.5.BUILD-SNAPSHOT + 1.1.8.BUILD-SNAPSHOT 4.0.0