Merge branch '3.0.x'
This commit is contained in:
@@ -222,31 +222,62 @@ NOTE: `spring.application.name` has no effect as far as the name registered for
|
||||
|
||||
'''
|
||||
|
||||
Spring Cloud Kubernetes can also watch the Kubernetes service catalog for changes and update the
|
||||
`DiscoveryClient` implementation accordingly. By "watch" we mean that we will publish a heartbeat event every `spring.cloud.kubernetes.discovery.catalog-services-watch-delay`
|
||||
milliseconds (by default it is `30000`). The heartbeat event will contain the target references (and their namespaces of the addresses of all endpoints
|
||||
Spring Cloud Kubernetes can also watch the Kubernetes service catalog for changes and update the `DiscoveryClient` implementation accordingly. In order to enable this functionality you need to add
|
||||
`@EnableScheduling` on a configuration class in your application. By "watch", we mean that we will publish a heartbeat event every `spring.cloud.kubernetes.discovery.catalog-services-watch-delay`
|
||||
milliseconds (by default it is `30000`). For the http discovery server this must be an environment variable set in deployment yaml:
|
||||
|
||||
----
|
||||
containers:
|
||||
- name: discovery-server
|
||||
image: springcloud/spring-cloud-kubernetes-discoveryserver:3.0.5-SNAPSHOT
|
||||
env:
|
||||
- name: SPRING_CLOUD_KUBERNETES_DISCOVERY_CATALOGSERVICESWATCHDELAY
|
||||
value: 3000
|
||||
----
|
||||
|
||||
The heartbeat event will contain the target references (and their namespaces of the addresses of all endpoints
|
||||
(for the exact details of what will get returned you can take a look inside `KubernetesCatalogWatch`). This is an implementation detail, and listeners of the heartbeat event
|
||||
should not rely on the details. Instead, they should see if there are differences between two subsequent heartbeats via `equals` method. We will take care to return a correct implementation that adheres to the equals contract.
|
||||
The endpoints will be queried in either :
|
||||
- all namespaces (enabled via `spring.cloud.kubernetes.discovery.all-namespaces=true`)
|
||||
- `all-namespaces` (enabled via `spring.cloud.kubernetes.discovery.all-namespaces=true`)
|
||||
|
||||
- specific namespaces (enabled via `spring.cloud.kubernetes.discovery.namespaces`), for example:
|
||||
- `selective namespaces` (enabled via `spring.cloud.kubernetes.discovery.namespaces`), for example:
|
||||
|
||||
- `one namespace` via xref:property-source-config.adoc#namespace-resolution[Namespace Resolution] if the above two paths are not taken.
|
||||
|
||||
NOTE: If, for any reasons, you want to disable catalog watcher, you need to set `spring.cloud.kubernetes.discovery.catalog-services-watch.enabled=false`. For the http discovery server, this needs to be an environment variable set in deployment for example:
|
||||
|
||||
[source]
|
||||
----
|
||||
spring:
|
||||
cloud:
|
||||
kubernetes:
|
||||
discovery:
|
||||
namespaces:
|
||||
- namespace-a
|
||||
- namespace-b
|
||||
SPRING_CLOUD_KUBERNETES_DISCOVERY_CATALOGSERVICESWATCH_ENABLED=FALSE
|
||||
----
|
||||
|
||||
- we will use: xref:property-source-config.adoc#namespace-resolution[Namespace Resolution] if the above two paths are not taken.
|
||||
The functionality of catalog watch works for all 3 discovery clients that we support, with some caveats that you need to be aware of in case of the http client.
|
||||
|
||||
In order to enable this functionality you need to add
|
||||
`@EnableScheduling` on a configuration class in your application.
|
||||
- The first is that this functionality is disabled by default, and it needs to be enabled in two places:
|
||||
|
||||
* in discovery server via an environment variable in the deployment manifest, for example:
|
||||
+
|
||||
----
|
||||
containers:
|
||||
- name: discovery-server
|
||||
image: springcloud/spring-cloud-kubernetes-discoveryserver:3.0.5-SNAPSHOT
|
||||
env:
|
||||
- name: SPRING_CLOUD_KUBERNETES_HTTP_DISCOVERY_CATALOG_WATCHER_ENABLED
|
||||
value: "TRUE"
|
||||
----
|
||||
+
|
||||
|
||||
* in discovery client, via a property in your `application.properties` for example:
|
||||
+
|
||||
----
|
||||
spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=true
|
||||
----
|
||||
+
|
||||
|
||||
- The second point is that this is only supported since version `3.0.6` and upwards.
|
||||
- Since http discovery has _two_ components : server and client, we strongly recommend to align versions between them, otherwise things might not work.
|
||||
- If you decide to disable catalog watcher, you need to disable it in both server and client.
|
||||
|
||||
By default, we use the `Endpoints`(see https://kubernetes.io/docs/concepts/services-networking/service/#endpoints) API to find out the current state of services. There is another way though, via `EndpointSlices` (https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/). Such support can be enabled via a property: `spring.cloud.kubernetes.discovery.use-endpoint-slices=true` (by default it is `false`). Of course, your cluster has to support it also. As a matter of fact, if you enable this property, but your cluster does not support it, we will fail starting the application. If you decide to enable such support, you also need proper Role/ClusterRole set-up. For example:
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.springframework.context.ApplicationEventPublisherAware;
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
|
||||
import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.CATALOG_WATCH_PROPERTY_WITH_DEFAULT_VALUE;
|
||||
import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.DISCOVERY_GROUP;
|
||||
import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.DISCOVERY_VERSION;
|
||||
import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.ENDPOINT_SLICE;
|
||||
@@ -67,7 +68,7 @@ class KubernetesCatalogWatch implements ApplicationEventPublisherAware {
|
||||
this.publisher = publisher;
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelayString = "${spring.cloud.kubernetes.discovery.catalogServicesWatchDelay:30000}")
|
||||
@Scheduled(fixedDelayString = "${" + CATALOG_WATCH_PROPERTY_WITH_DEFAULT_VALUE + "}")
|
||||
void catalogServicesWatch() {
|
||||
try {
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2019-2023 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.commons.discovery;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
|
||||
/**
|
||||
* Provides a more succinct conditional for:
|
||||
* <code>spring.cloud.kubernetes.http.discovery.client.catalog.watcher.enabled</code>.
|
||||
*
|
||||
* @author wind57
|
||||
*/
|
||||
@Target({ ElementType.TYPE, ElementType.METHOD })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Inherited
|
||||
@ConditionalOnProperty(value = "spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled", matchIfMissing = false)
|
||||
public @interface ConditionalOnHttpDiscoveryCatalogWatcherEnabled {
|
||||
|
||||
}
|
||||
@@ -82,4 +82,20 @@ public final class KubernetesDiscoveryConstants {
|
||||
*/
|
||||
public static final String SECURED = "secured";
|
||||
|
||||
/**
|
||||
* catalog watch delay property name.
|
||||
*/
|
||||
public static final String CATALOG_WATCH_PROPERTY_NAME = "spring.cloud.kubernetes.discovery.catalogServicesWatchDelay";
|
||||
|
||||
/**
|
||||
* default delay for the configuration watcher.
|
||||
*/
|
||||
public static final String CATALOG_WATCHER_DEFAULT_DELAY = "30000";
|
||||
|
||||
/**
|
||||
* catalog watch delay property name with default value.
|
||||
*/
|
||||
public static final String CATALOG_WATCH_PROPERTY_WITH_DEFAULT_VALUE = CATALOG_WATCH_PROPERTY_NAME + ":"
|
||||
+ CATALOG_WATCHER_DEFAULT_DELAY;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2013-2023 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.discoveryserver;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnHttpDiscoveryCatalogWatcherEnabled;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKubernetesCatalogEnabled;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
@RestController
|
||||
@ConditionalOnKubernetesCatalogEnabled
|
||||
@ConditionalOnHttpDiscoveryCatalogWatcherEnabled
|
||||
class DiscoveryCatalogWatcherController {
|
||||
|
||||
private final HeartBeatListener heartBeatListener;
|
||||
|
||||
DiscoveryCatalogWatcherController(HeartBeatListener heartBeatListener) {
|
||||
this.heartBeatListener = heartBeatListener;
|
||||
}
|
||||
|
||||
@GetMapping("/state")
|
||||
Mono<List<EndpointNameAndNamespace>> state() {
|
||||
return Mono.defer(() -> Mono.just(heartBeatListener.lastState().get()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,11 +18,13 @@ package org.springframework.cloud.kubernetes.discoveryserver;
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/**
|
||||
* @author Ryan Baxter
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
public class DiscoveryServerApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2013-2023 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.discoveryserver;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.cloud.client.discovery.event.HeartbeatEvent;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnHttpDiscoveryCatalogWatcherEnabled;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKubernetesCatalogEnabled;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.CATALOG_WATCHER_DEFAULT_DELAY;
|
||||
import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.CATALOG_WATCH_PROPERTY_NAME;
|
||||
|
||||
/**
|
||||
* Listener for a HeartbeatEvent that comes from KubernetesCatalogWatch.
|
||||
*
|
||||
* @author wind57
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnKubernetesCatalogEnabled
|
||||
@ConditionalOnHttpDiscoveryCatalogWatcherEnabled
|
||||
class HeartBeatListener implements ApplicationListener<HeartbeatEvent> {
|
||||
|
||||
private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(HeartBeatListener.class));
|
||||
|
||||
private final AtomicReference<List<EndpointNameAndNamespace>> lastState = new AtomicReference<>(List.of());
|
||||
|
||||
HeartBeatListener(Environment environment) {
|
||||
String watchDelay = environment.getProperty(CATALOG_WATCH_PROPERTY_NAME);
|
||||
if (watchDelay != null) {
|
||||
LOG.debug("using delay : " + watchDelay);
|
||||
}
|
||||
else {
|
||||
LOG.debug("using default watch delay : " + CATALOG_WATCHER_DEFAULT_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void onApplicationEvent(HeartbeatEvent event) {
|
||||
LOG.debug(() -> "received heartbeat event");
|
||||
List<EndpointNameAndNamespace> state = (List<EndpointNameAndNamespace>) event.getValue();
|
||||
LOG.debug(() -> "state received : " + state);
|
||||
lastState.set(state);
|
||||
}
|
||||
|
||||
AtomicReference<List<EndpointNameAndNamespace>> lastState() {
|
||||
return lastState;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2013-2023 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.discoveryserver;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
class DiscoveryCatalogWatcherControllerTests {
|
||||
|
||||
private final HeartBeatListener heartBeatListener = Mockito.mock(HeartBeatListener.class);
|
||||
|
||||
@Test
|
||||
void test() {
|
||||
Mockito.when(heartBeatListener.lastState())
|
||||
.thenReturn(new AtomicReference<>(List.of(new EndpointNameAndNamespace("one", "two"))));
|
||||
|
||||
DiscoveryCatalogWatcherController catalogWatcherController = new DiscoveryCatalogWatcherController(
|
||||
heartBeatListener);
|
||||
|
||||
StepVerifier.create(catalogWatcherController.state())
|
||||
.expectNext(List.of(new EndpointNameAndNamespace("one", "two"))).verifyComplete();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright 2013-2023 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.discoveryserver;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.cloud.kubernetes.client.discovery.reactive.KubernetesInformerReactiveDiscoveryClient;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
class DiscoveryServerApplicationContextTests {
|
||||
|
||||
@Nested
|
||||
@SpringBootTest(classes = TestConfig.class,
|
||||
properties = "spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=true")
|
||||
class BothControllersPresent {
|
||||
|
||||
@Autowired
|
||||
private ObjectProvider<DiscoveryServerController> discoveryServerController;
|
||||
|
||||
@Autowired
|
||||
private ObjectProvider<DiscoveryCatalogWatcherController> discoveryCatalogWatcherController;
|
||||
|
||||
@Autowired
|
||||
private ObjectProvider<HeartBeatListener> heartBeatListener;
|
||||
|
||||
@Test
|
||||
void test() {
|
||||
Assertions.assertNotNull(discoveryServerController.getIfAvailable());
|
||||
Assertions.assertNotNull(discoveryCatalogWatcherController.getIfAvailable());
|
||||
Assertions.assertNotNull(heartBeatListener.getIfAvailable());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
@SpringBootTest(classes = TestConfig.class,
|
||||
properties = { "spring.cloud.kubernetes.discovery.catalog-services-watch.enabled=false",
|
||||
"spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=true" })
|
||||
class CatalogControllerNotPresentOne {
|
||||
|
||||
@Autowired
|
||||
private ObjectProvider<DiscoveryServerController> discoveryServerController;
|
||||
|
||||
@Autowired
|
||||
private ObjectProvider<DiscoveryCatalogWatcherController> discoveryCatalogWatcherController;
|
||||
|
||||
@Autowired
|
||||
private ObjectProvider<HeartBeatListener> heartBeatListener;
|
||||
|
||||
@Test
|
||||
void test() {
|
||||
Assertions.assertNotNull(discoveryServerController.getIfAvailable());
|
||||
Assertions.assertNull(discoveryCatalogWatcherController.getIfAvailable());
|
||||
Assertions.assertNull(heartBeatListener.getIfAvailable());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
@SpringBootTest(classes = TestConfig.class,
|
||||
properties = { "spring.cloud.kubernetes.discovery.catalog-services-watch.enabled=true",
|
||||
"spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=false" })
|
||||
class CatalogControllerNotPresentTwo {
|
||||
|
||||
@Autowired
|
||||
private ObjectProvider<DiscoveryServerController> discoveryServerController;
|
||||
|
||||
@Autowired
|
||||
private ObjectProvider<DiscoveryCatalogWatcherController> discoveryCatalogWatcherController;
|
||||
|
||||
@Autowired
|
||||
private ObjectProvider<HeartBeatListener> heartBeatListener;
|
||||
|
||||
@Test
|
||||
void test() {
|
||||
Assertions.assertNotNull(discoveryServerController.getIfAvailable());
|
||||
Assertions.assertNull(discoveryCatalogWatcherController.getIfAvailable());
|
||||
Assertions.assertNull(heartBeatListener.getIfAvailable());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
@SpringBootTest(classes = TestConfig.class,
|
||||
properties = { "spring.cloud.kubernetes.discovery.catalog-services-watch.enabled=false",
|
||||
"spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=false" })
|
||||
class CatalogControllerNotPresentThree {
|
||||
|
||||
@Autowired
|
||||
private ObjectProvider<DiscoveryServerController> discoveryServerController;
|
||||
|
||||
@Autowired
|
||||
private ObjectProvider<DiscoveryCatalogWatcherController> discoveryCatalogWatcherController;
|
||||
|
||||
@Autowired
|
||||
private ObjectProvider<HeartBeatListener> heartBeatListener;
|
||||
|
||||
@Test
|
||||
void test() {
|
||||
Assertions.assertNotNull(discoveryServerController.getIfAvailable());
|
||||
Assertions.assertNull(discoveryCatalogWatcherController.getIfAvailable());
|
||||
Assertions.assertNull(heartBeatListener.getIfAvailable());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@TestConfiguration
|
||||
static class TestConfig {
|
||||
|
||||
@Bean
|
||||
KubernetesInformerReactiveDiscoveryClient discoveryClient() {
|
||||
return Mockito.mock(KubernetesInformerReactiveDiscoveryClient.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2013-2023 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.discoveryserver;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.cloud.client.discovery.event.HeartbeatEvent;
|
||||
import org.springframework.cloud.kubernetes.client.discovery.reactive.KubernetesInformerReactiveDiscoveryClient;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.ApplicationEventPublisherAware;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
@SpringBootTest(classes = HeartbeatTest.TestConfig.class,
|
||||
properties = "spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=true")
|
||||
@AutoConfigureWebTestClient
|
||||
class HeartbeatTest {
|
||||
|
||||
@Autowired
|
||||
private WebTestClient client;
|
||||
|
||||
@Autowired
|
||||
private ApplicationContext context;
|
||||
|
||||
@Test
|
||||
void testHeartbeat() {
|
||||
Assertions.assertTrue(true);
|
||||
client.get().uri("/state").exchange().expectStatus().is2xxSuccessful().expectBody().json("[]");
|
||||
|
||||
context.getBean(HeartbeatPublisher.class).publishEvent();
|
||||
client.get().uri("/state").exchange().expectStatus().is2xxSuccessful().expectBody().json("""
|
||||
[
|
||||
{
|
||||
"endpointName":"endpoint-name",
|
||||
"namespace":"namespaceA"
|
||||
}
|
||||
]
|
||||
""");
|
||||
}
|
||||
|
||||
@TestConfiguration
|
||||
static class TestConfig {
|
||||
|
||||
@Bean
|
||||
KubernetesInformerReactiveDiscoveryClient client() {
|
||||
return Mockito.mock(KubernetesInformerReactiveDiscoveryClient.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
HeartbeatPublisher heartbeatPublisher() {
|
||||
return new HeartbeatPublisher();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class HeartbeatPublisher implements ApplicationEventPublisherAware {
|
||||
|
||||
private ApplicationEventPublisher publisher;
|
||||
|
||||
@Override
|
||||
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
|
||||
this.publisher = publisher;
|
||||
}
|
||||
|
||||
void publishEvent() {
|
||||
publisher.publishEvent(
|
||||
new HeartbeatEvent("test", List.of(new EndpointNameAndNamespace("endpoint-name", "namespaceA"))));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -79,7 +79,7 @@ class ConfigServerBootstrapper extends KubernetesConfigServerBootstrapper {
|
||||
.orElseGet(() -> KubernetesDiscoveryProperties.DEFAULT);
|
||||
KubernetesDiscoveryClientBlockingAutoConfiguration autoConfiguration = new KubernetesDiscoveryClientBlockingAutoConfiguration();
|
||||
DiscoveryClient discoveryClient = autoConfiguration
|
||||
.kubernetesDiscoveryClient(autoConfiguration.restTemplate(), kubernetesDiscoveryProperties);
|
||||
.kubernetesDiscoveryClient(autoConfiguration.restTemplateBuilder(), kubernetesDiscoveryProperties);
|
||||
return discoveryClient::getInstances;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2013-2023 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.discovery;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.cloud.client.discovery.event.HeartbeatEvent;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.ApplicationEventPublisherAware;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.CATALOG_WATCH_PROPERTY_WITH_DEFAULT_VALUE;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
final class KubernetesCatalogWatch implements ApplicationEventPublisherAware {
|
||||
|
||||
private static final ParameterizedTypeReference<List<EndpointNameAndNamespace>> TYPE = new ParameterizedTypeReference<>() {
|
||||
|
||||
};
|
||||
|
||||
private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(KubernetesCatalogWatch.class));
|
||||
|
||||
private final AtomicReference<List<EndpointNameAndNamespace>> catalogState = new AtomicReference<>(List.of());
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
private ApplicationEventPublisher publisher;
|
||||
|
||||
KubernetesCatalogWatch(RestTemplateBuilder builder, KubernetesDiscoveryProperties properties) {
|
||||
this.restTemplate = builder.rootUri(properties.discoveryServerUrl()).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
|
||||
this.publisher = publisher;
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelayString = "${" + CATALOG_WATCH_PROPERTY_WITH_DEFAULT_VALUE + "}")
|
||||
public void catalogServicesWatch() {
|
||||
try {
|
||||
List<EndpointNameAndNamespace> currentState = restTemplate.exchange("/state", HttpMethod.GET, null, TYPE)
|
||||
.getBody();
|
||||
|
||||
if (!catalogState.get().equals(currentState)) {
|
||||
LOG.debug(() -> "Received update from kubernetes discovery http client: " + currentState);
|
||||
publisher.publishEvent(new HeartbeatEvent(this, currentState));
|
||||
}
|
||||
|
||||
catalogState.set(currentState);
|
||||
}
|
||||
catch (Exception e) {
|
||||
LOG.error(e, () -> "Error watching Kubernetes Services");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2013-2023 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.discovery;
|
||||
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.cloud.CloudPlatform;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnHttpDiscoveryCatalogWatcherEnabled;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKubernetesCatalogEnabled;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
|
||||
import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.CATALOG_WATCHER_DEFAULT_DELAY;
|
||||
import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.CATALOG_WATCH_PROPERTY_NAME;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnDiscoveryEnabled
|
||||
@ConditionalOnKubernetesCatalogEnabled
|
||||
@ConditionalOnHttpDiscoveryCatalogWatcherEnabled
|
||||
@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES)
|
||||
@EnableConfigurationProperties(KubernetesDiscoveryProperties.class)
|
||||
class KubernetesCatalogWatchAutoConfiguration {
|
||||
|
||||
private static final LogAccessor LOG = new LogAccessor(
|
||||
LogFactory.getLog(KubernetesCatalogWatchAutoConfiguration.class));
|
||||
|
||||
// this has to be a RestTemplateBuilder and not a WebClientBuilder, otherwise
|
||||
// we need the webflux dependency, and it might leak into client's dependencies
|
||||
// which is not always desirable.
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
RestTemplateBuilder restTemplateBuilder() {
|
||||
return new RestTemplateBuilder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
KubernetesCatalogWatch kubernetesCatalogWatch(RestTemplateBuilder builder, KubernetesDiscoveryProperties properties,
|
||||
Environment environment) {
|
||||
|
||||
String watchDelay = environment.getProperty(CATALOG_WATCH_PROPERTY_NAME);
|
||||
if (watchDelay != null) {
|
||||
LOG.debug("using delay : " + watchDelay);
|
||||
}
|
||||
else {
|
||||
LOG.debug("using default watch delay : " + CATALOG_WATCHER_DEFAULT_DELAY);
|
||||
}
|
||||
|
||||
return new KubernetesCatalogWatch(builder, properties);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,7 +28,6 @@ import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscover
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
@@ -41,15 +40,15 @@ class KubernetesDiscoveryClientBlockingAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
RestTemplate restTemplate() {
|
||||
return new RestTemplateBuilder().build();
|
||||
RestTemplateBuilder restTemplateBuilder() {
|
||||
return new RestTemplateBuilder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
KubernetesDiscoveryClient kubernetesDiscoveryClient(RestTemplate restTemplate,
|
||||
KubernetesDiscoveryClient kubernetesDiscoveryClient(RestTemplateBuilder restTemplateBuilder,
|
||||
KubernetesDiscoveryProperties properties) {
|
||||
return new KubernetesDiscoveryClient(restTemplate, properties);
|
||||
return new KubernetesDiscoveryClient(restTemplateBuilder.build(), properties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -40,14 +40,14 @@ public class KubernetesReactiveDiscoveryClient implements ReactiveDiscoveryClien
|
||||
if (!StringUtils.hasText(properties.getDiscoveryServerUrl())) {
|
||||
throw new DiscoveryServerUrlInvalidException();
|
||||
}
|
||||
this.webClient = webClientBuilder.baseUrl(properties.getDiscoveryServerUrl()).build();
|
||||
webClient = webClientBuilder.baseUrl(properties.getDiscoveryServerUrl()).build();
|
||||
}
|
||||
|
||||
KubernetesReactiveDiscoveryClient(WebClient.Builder webClientBuilder, KubernetesDiscoveryProperties properties) {
|
||||
if (!StringUtils.hasText(properties.discoveryServerUrl())) {
|
||||
throw new DiscoveryServerUrlInvalidException();
|
||||
}
|
||||
this.webClient = webClientBuilder.baseUrl(properties.discoveryServerUrl()).build();
|
||||
webClient = webClientBuilder.baseUrl(properties.discoveryServerUrl()).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryClientBlockingAutoConfiguration
|
||||
org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryClientReactiveAutoConfiguration
|
||||
org.springframework.cloud.kubernetes.discovery.KubernetesCatalogWatchAutoConfiguration
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright 2013-2023 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.discovery;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
class KubernetesCatalogWatchAutoConfigurationTests {
|
||||
|
||||
private ApplicationContextRunner applicationContextRunner;
|
||||
|
||||
@Test
|
||||
void discoveryEnabledDefault() {
|
||||
setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false",
|
||||
"spring.cloud.kubernetes.discovery.discovery-server-url=example.com",
|
||||
"spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=true");
|
||||
applicationContextRunner.run(context -> assertThat(context).hasSingleBean(KubernetesCatalogWatch.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void discoveryEnabled() {
|
||||
setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false",
|
||||
"spring.cloud.discovery.enabled=true",
|
||||
"spring.cloud.kubernetes.discovery.discovery-server-url=example.com",
|
||||
"spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=true");
|
||||
applicationContextRunner.run(context -> assertThat(context).hasSingleBean(KubernetesCatalogWatch.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void discoveryDisabled() {
|
||||
setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false",
|
||||
"spring.cloud.discovery.enabled=false",
|
||||
"spring.cloud.kubernetes.discovery.discovery-server-url=example.com");
|
||||
applicationContextRunner.run(context -> assertThat(context).doesNotHaveBean(KubernetesCatalogWatch.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void kubernetesDiscoveryEnabled() {
|
||||
setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false",
|
||||
"spring.cloud.kubernetes.discovery.enabled=true",
|
||||
"spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=true",
|
||||
"spring.cloud.kubernetes.discovery.discovery-server-url=example.com");
|
||||
applicationContextRunner.run(context -> assertThat(context).hasSingleBean(KubernetesCatalogWatch.class));
|
||||
}
|
||||
|
||||
// disabling discovery has no impact on the catalog watch.
|
||||
@Test
|
||||
void kubernetesDiscoveryDisabled() {
|
||||
setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false",
|
||||
"spring.cloud.kubernetes.discovery.enabled=false",
|
||||
"spring.cloud.kubernetes.discovery.discovery-server-url=example.com",
|
||||
"spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=true");
|
||||
applicationContextRunner.run(context -> assertThat(context).hasSingleBean(KubernetesCatalogWatch.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* both blocking and reactive configs are disabled, should not influence catalog
|
||||
* watcher in any way.
|
||||
*/
|
||||
@Test
|
||||
void disableBlockingAndReactive() {
|
||||
setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false",
|
||||
"spring.cloud.discovery.blocking.enabled=false", "spring.cloud.discovery.reactive.enabled=false",
|
||||
"spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=true");
|
||||
applicationContextRunner.run(context -> {
|
||||
assertThat(context).hasSingleBean(KubernetesCatalogWatch.class);
|
||||
assertThat(context).doesNotHaveBean(KubernetesDiscoveryClient.class);
|
||||
assertThat(context).doesNotHaveBean(KubernetesReactiveDiscoveryClient.class);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* spring.cloud.kubernetes.discovery.enabled is false, but does not influence catalog
|
||||
* watcher.
|
||||
*/
|
||||
@Test
|
||||
void disableKubernetesDiscovery() {
|
||||
setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false",
|
||||
"spring.cloud.kubernetes.discovery.enabled=false",
|
||||
"spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=true");
|
||||
applicationContextRunner.run(context -> {
|
||||
assertThat(context).hasSingleBean(KubernetesCatalogWatch.class);
|
||||
assertThat(context).doesNotHaveBean(KubernetesDiscoveryClient.class);
|
||||
assertThat(context).doesNotHaveBean(KubernetesReactiveDiscoveryClient.class);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* spring.cloud.kubernetes.http.discovery.client.catalog.watcher.enabled is false, as
|
||||
* such catalog watcher is not present.
|
||||
*/
|
||||
@Test
|
||||
void disableHttpDiscoveryClientCatalogWatcher() {
|
||||
setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=true",
|
||||
"spring.cloud.kubernetes.discovery.enabled=false",
|
||||
"spring.cloud.kubernetes.http.discovery.catalog.watcher.enabled=false");
|
||||
applicationContextRunner.run(context -> {
|
||||
assertThat(context).doesNotHaveBean(KubernetesCatalogWatch.class);
|
||||
assertThat(context).doesNotHaveBean(KubernetesDiscoveryClient.class);
|
||||
assertThat(context).doesNotHaveBean(KubernetesReactiveDiscoveryClient.class);
|
||||
});
|
||||
}
|
||||
|
||||
private void setup(String... properties) {
|
||||
applicationContextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(KubernetesCatalogWatchAutoConfiguration.class,
|
||||
KubernetesDiscoveryClientBlockingAutoConfiguration.class,
|
||||
KubernetesDiscoveryClientReactiveAutoConfiguration.class))
|
||||
.withPropertyValues(properties);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* Copyright 2013-2023 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.discovery;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.client.WireMock;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.cloud.client.discovery.event.HeartbeatEvent;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.get;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
|
||||
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
class KubernetesCatalogWatchTests {
|
||||
|
||||
private WireMockServer wireMockServer;
|
||||
|
||||
private static final ArgumentCaptor<HeartbeatEvent> HEARTBEAT_EVENT_ARGUMENT_CAPTOR = ArgumentCaptor
|
||||
.forClass(HeartbeatEvent.class);
|
||||
|
||||
private static final ApplicationEventPublisher APPLICATION_EVENT_PUBLISHER = Mockito
|
||||
.mock(ApplicationEventPublisher.class);
|
||||
|
||||
@AfterEach
|
||||
void afterEach() {
|
||||
Mockito.reset(APPLICATION_EVENT_PUBLISHER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSingleCycleSameAsCurrentState() {
|
||||
|
||||
String body = "[]";
|
||||
|
||||
wireMockServer = new WireMockServer(options().dynamicPort());
|
||||
wireMockServer.start();
|
||||
WireMock.configureFor(wireMockServer.port());
|
||||
stubFor(get("/state")
|
||||
.willReturn(aResponse().withStatus(200).withBody(body).withHeader("content-type", "application/json")));
|
||||
|
||||
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60,
|
||||
false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false, false,
|
||||
wireMockServer.baseUrl());
|
||||
|
||||
KubernetesCatalogWatch catalogWatch = new KubernetesCatalogWatch(new RestTemplateBuilder(), properties);
|
||||
catalogWatch.setApplicationEventPublisher(APPLICATION_EVENT_PUBLISHER);
|
||||
|
||||
catalogWatch.catalogServicesWatch();
|
||||
|
||||
Mockito.verifyNoInteractions(APPLICATION_EVENT_PUBLISHER);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void testSingleCycleDifferentCurrentState() {
|
||||
|
||||
String body = """
|
||||
[
|
||||
{
|
||||
"endpointName":"endpoint-name",
|
||||
"namespace":"namespaceA"
|
||||
}
|
||||
]
|
||||
""";
|
||||
|
||||
wireMockServer = new WireMockServer(options().dynamicPort());
|
||||
wireMockServer.start();
|
||||
WireMock.configureFor(wireMockServer.port());
|
||||
stubFor(get("/state")
|
||||
.willReturn(aResponse().withStatus(200).withBody(body).withHeader("content-type", "application/json")));
|
||||
|
||||
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60,
|
||||
false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false, false,
|
||||
wireMockServer.baseUrl());
|
||||
|
||||
KubernetesCatalogWatch catalogWatch = new KubernetesCatalogWatch(new RestTemplateBuilder(), properties);
|
||||
catalogWatch.setApplicationEventPublisher(APPLICATION_EVENT_PUBLISHER);
|
||||
|
||||
catalogWatch.catalogServicesWatch();
|
||||
|
||||
verify(APPLICATION_EVENT_PUBLISHER).publishEvent(HEARTBEAT_EVENT_ARGUMENT_CAPTOR.capture());
|
||||
HeartbeatEvent event = HEARTBEAT_EVENT_ARGUMENT_CAPTOR.getValue();
|
||||
Assertions.assertEquals(event.getSource().getClass(), KubernetesCatalogWatch.class);
|
||||
|
||||
List<EndpointNameAndNamespace> state = (List<EndpointNameAndNamespace>) event.getValue();
|
||||
|
||||
Assertions.assertEquals(state.size(), 1);
|
||||
Assertions.assertEquals(state.get(0).namespace(), "namespaceA");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void testTwoCyclesDifferentStates() {
|
||||
|
||||
String bodyOne = """
|
||||
[
|
||||
{
|
||||
"endpointName":"endpoint-name",
|
||||
"namespace":"namespaceA"
|
||||
}
|
||||
]
|
||||
""";
|
||||
|
||||
// namespace differs
|
||||
String bodyTwo = """
|
||||
[
|
||||
{
|
||||
"endpointName":"endpoint-name",
|
||||
"namespace":"namespaceB"
|
||||
}
|
||||
]
|
||||
""";
|
||||
|
||||
wireMockServer = new WireMockServer(options().dynamicPort());
|
||||
wireMockServer.start();
|
||||
WireMock.configureFor(wireMockServer.port());
|
||||
stubFor(get("/state").willReturn(
|
||||
aResponse().withStatus(200).withBody(bodyOne).withHeader("content-type", "application/json")));
|
||||
|
||||
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60,
|
||||
false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false, false,
|
||||
wireMockServer.baseUrl());
|
||||
|
||||
KubernetesCatalogWatch catalogWatch = new KubernetesCatalogWatch(new RestTemplateBuilder(), properties);
|
||||
catalogWatch.setApplicationEventPublisher(APPLICATION_EVENT_PUBLISHER);
|
||||
|
||||
catalogWatch.catalogServicesWatch();
|
||||
|
||||
verify(APPLICATION_EVENT_PUBLISHER).publishEvent(HEARTBEAT_EVENT_ARGUMENT_CAPTOR.capture());
|
||||
HeartbeatEvent eventOne = HEARTBEAT_EVENT_ARGUMENT_CAPTOR.getValue();
|
||||
Assertions.assertEquals(eventOne.getSource().getClass(), KubernetesCatalogWatch.class);
|
||||
|
||||
List<EndpointNameAndNamespace> stateOne = (List<EndpointNameAndNamespace>) eventOne.getValue();
|
||||
|
||||
Assertions.assertEquals(stateOne.size(), 1);
|
||||
Assertions.assertEquals(stateOne.get(0).namespace(), "namespaceA");
|
||||
|
||||
// second call
|
||||
stubFor(get("/state").willReturn(
|
||||
aResponse().withStatus(200).withBody(bodyTwo).withHeader("content-type", "application/json")));
|
||||
|
||||
catalogWatch.catalogServicesWatch();
|
||||
|
||||
verify(APPLICATION_EVENT_PUBLISHER, Mockito.times(2)).publishEvent(HEARTBEAT_EVENT_ARGUMENT_CAPTOR.capture());
|
||||
HeartbeatEvent eventTwo = HEARTBEAT_EVENT_ARGUMENT_CAPTOR.getValue();
|
||||
Assertions.assertEquals(eventTwo.getSource().getClass(), KubernetesCatalogWatch.class);
|
||||
|
||||
List<EndpointNameAndNamespace> stateTwo = (List<EndpointNameAndNamespace>) eventTwo.getValue();
|
||||
|
||||
Assertions.assertEquals(stateTwo.size(), 1);
|
||||
Assertions.assertEquals(stateTwo.get(0).namespace(), "namespaceB");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import org.springframework.boot.actuate.health.HealthIndicator;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
@@ -103,7 +104,7 @@ class KubernetesDiscoveryAutoConfigurationTests {
|
||||
"spring.cloud.discovery.reactive.enabled=false",
|
||||
"spring.cloud.kubernetes.discovery.discovery-server-url=http://k8sdiscoveryserver");
|
||||
applicationContextRunner.run(context -> {
|
||||
assertThat(context).hasSingleBean(RestTemplate.class);
|
||||
assertThat(context).hasSingleBean(RestTemplateBuilder.class);
|
||||
assertThat(context).hasSingleBean(KubernetesDiscoveryClient.class);
|
||||
assertThat(context).getBean("indicatorInitializer").isNotNull();
|
||||
|
||||
@@ -147,7 +148,7 @@ class KubernetesDiscoveryAutoConfigurationTests {
|
||||
"spring.cloud.discovery.reactive.enabled=false",
|
||||
"spring.cloud.kubernetes.discovery.discovery-server-url=http://k8sdiscoveryserver");
|
||||
applicationContextRunner.run(context -> {
|
||||
assertThat(context).hasSingleBean(RestTemplate.class);
|
||||
assertThat(context).hasSingleBean(RestTemplateBuilder.class);
|
||||
assertThat(context).hasSingleBean(KubernetesDiscoveryClient.class);
|
||||
assertThat(context).doesNotHaveBean("indicatorInitializer");
|
||||
|
||||
@@ -169,7 +170,7 @@ class KubernetesDiscoveryAutoConfigurationTests {
|
||||
"spring.cloud.kubernetes.discovery.discovery-server-url=http://k8sdiscoveryserver",
|
||||
"spring.cloud.discovery.client.health-indicator.enabled=false");
|
||||
applicationContextRunner.run(context -> {
|
||||
assertThat(context).hasSingleBean(RestTemplate.class);
|
||||
assertThat(context).hasSingleBean(RestTemplateBuilder.class);
|
||||
assertThat(context).hasSingleBean(KubernetesDiscoveryClient.class);
|
||||
assertThat(context).doesNotHaveBean("indicatorInitializer");
|
||||
|
||||
@@ -191,7 +192,7 @@ class KubernetesDiscoveryAutoConfigurationTests {
|
||||
"spring.cloud.discovery.reactive.enabled=true",
|
||||
"spring.cloud.kubernetes.discovery.discovery-server-url=http://k8sdiscoveryserver");
|
||||
applicationContextRunner.run(context -> {
|
||||
assertThat(context).hasSingleBean(RestTemplate.class);
|
||||
assertThat(context).hasSingleBean(RestTemplateBuilder.class);
|
||||
assertThat(context).hasSingleBean(KubernetesDiscoveryClient.class);
|
||||
assertThat(context).getBean("indicatorInitializer").isNotNull();
|
||||
|
||||
@@ -213,7 +214,7 @@ class KubernetesDiscoveryAutoConfigurationTests {
|
||||
"spring.cloud.discovery.reactive.enabled=true",
|
||||
"spring.cloud.kubernetes.discovery.discovery-server-url=http://k8sdiscoveryserver");
|
||||
applicationContextRunner.run(context -> {
|
||||
assertThat(context).hasSingleBean(RestTemplate.class);
|
||||
assertThat(context).hasSingleBean(RestTemplateBuilder.class);
|
||||
assertThat(context).hasSingleBean(KubernetesDiscoveryClient.class);
|
||||
assertThat(context).getBean("indicatorInitializer").isNotNull();
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.springframework.context.ApplicationEventPublisherAware;
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
|
||||
import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.CATALOG_WATCH_PROPERTY_WITH_DEFAULT_VALUE;
|
||||
import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.DISCOVERY_GROUP;
|
||||
import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.DISCOVERY_VERSION;
|
||||
import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.ENDPOINT_SLICE;
|
||||
@@ -66,7 +67,7 @@ public class KubernetesCatalogWatch implements ApplicationEventPublisherAware {
|
||||
this.publisher = publisher;
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelayString = "${spring.cloud.kubernetes.discovery.catalogServicesWatchDelay:30000}")
|
||||
@Scheduled(fixedDelayString = "${" + CATALOG_WATCH_PROPERTY_WITH_DEFAULT_VALUE + "}")
|
||||
public void catalogServicesWatch() {
|
||||
try {
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2013-2023 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.discoveryclient.it;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.cloud.client.discovery.event.HeartbeatEvent;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
@Component
|
||||
class HeartbeatListener implements ApplicationListener<HeartbeatEvent> {
|
||||
|
||||
private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(HeartbeatListener.class));
|
||||
|
||||
AtomicReference<List<EndpointNameAndNamespace>> state = new AtomicReference<>(List.of());
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void onApplicationEvent(HeartbeatEvent event) {
|
||||
LOG.info("received heartbeat event");
|
||||
List<EndpointNameAndNamespace> state = (List<EndpointNameAndNamespace>) event.getValue();
|
||||
this.state.set(state);
|
||||
LOG.info("state received : " + state);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,6 +22,8 @@ import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.discovery.DiscoveryClient;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@@ -30,13 +32,17 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
* @author Ryan Baxter
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
@RestController
|
||||
public class KubernetesDiscoveryClientApplicationIt {
|
||||
class KubernetesDiscoveryClientApplicationIt {
|
||||
|
||||
private final DiscoveryClient discoveryClient;
|
||||
|
||||
public KubernetesDiscoveryClientApplicationIt(DiscoveryClient discoveryClient) {
|
||||
private final HeartbeatListener heartbeatListener;
|
||||
|
||||
KubernetesDiscoveryClientApplicationIt(DiscoveryClient discoveryClient, HeartbeatListener heartbeatListener) {
|
||||
this.discoveryClient = discoveryClient;
|
||||
this.heartbeatListener = heartbeatListener;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
@@ -44,13 +50,18 @@ public class KubernetesDiscoveryClientApplicationIt {
|
||||
}
|
||||
|
||||
@GetMapping("/services")
|
||||
public List<String> services() {
|
||||
List<String> services() {
|
||||
return discoveryClient.getServices();
|
||||
}
|
||||
|
||||
@GetMapping("/service/{serviceId}")
|
||||
public List<ServiceInstance> service(@PathVariable String serviceId) {
|
||||
List<ServiceInstance> service(@PathVariable String serviceId) {
|
||||
return discoveryClient.getInstances(serviceId);
|
||||
}
|
||||
|
||||
@GetMapping("/state")
|
||||
List<EndpointNameAndNamespace> state() {
|
||||
return heartbeatListener.state.get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,14 +17,18 @@
|
||||
package org.springframework.cloud.kubernetes.discoveryclient.it;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.assertj.core.api.Condition;
|
||||
import org.testcontainers.k3s.K3sContainer;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
import reactor.util.retry.Retry;
|
||||
import reactor.util.retry.RetryBackoffSpec;
|
||||
|
||||
import org.springframework.boot.test.json.BasicJsonTester;
|
||||
import org.springframework.cloud.kubernetes.integration.tests.commons.Commons;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
@@ -34,12 +38,17 @@ import org.springframework.web.reactive.function.client.WebClient;
|
||||
*/
|
||||
final class DiscoveryClientFilterNamespaceDelegate {
|
||||
|
||||
private DiscoveryClientFilterNamespaceDelegate() {
|
||||
|
||||
}
|
||||
|
||||
private static final BasicJsonTester BASIC_JSON_TESTER = new BasicJsonTester(
|
||||
DiscoveryClientFilterNamespaceDelegate.class);
|
||||
|
||||
static void testNamespaceDiscoveryClient() {
|
||||
static void testNamespaceDiscoveryClient(K3sContainer container) {
|
||||
testLoadBalancer();
|
||||
testHealth();
|
||||
testForHeartbeat(container);
|
||||
}
|
||||
|
||||
private static void testLoadBalancer() {
|
||||
@@ -76,6 +85,42 @@ final class DiscoveryClientFilterNamespaceDelegate {
|
||||
.extractingJsonPathStringValue("$.components.discoveryComposite.status").isEqualTo("UP");
|
||||
}
|
||||
|
||||
private static void testForHeartbeat(K3sContainer container) {
|
||||
|
||||
// 1. logs from discovery server
|
||||
Commons.waitForLogStatement("using delay : 3000", container, "spring-cloud-kubernetes-discoveryserver");
|
||||
Commons.waitForLogStatement("received heartbeat event", container, "spring-cloud-kubernetes-discoveryserver");
|
||||
Commons.waitForLogStatement("state received :", container, "spring-cloud-kubernetes-discoveryserver");
|
||||
|
||||
// 2. logs from discovery client
|
||||
Commons.waitForLogStatement("using delay : 3000", container,
|
||||
"spring-cloud-kubernetes-k8s-client-discovery-server");
|
||||
Commons.waitForLogStatement("state received : ", container,
|
||||
"spring-cloud-kubernetes-k8s-client-discovery-server");
|
||||
|
||||
// 3. heartbeat listener message
|
||||
WebClient.Builder builder = builder();
|
||||
WebClient client = builder.baseUrl("http://localhost:80/discoveryclient-it/state").build();
|
||||
String result = client.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec())
|
||||
.block();
|
||||
|
||||
Condition<LinkedHashMap<String, String>> wireMockService = new Condition<>(
|
||||
map -> map.entrySet().stream().anyMatch(en -> en.getValue().contains("service-wiremock-deployment")),
|
||||
"");
|
||||
|
||||
Condition<LinkedHashMap<String, String>> discoveryServerService = new Condition<>(
|
||||
map -> map.entrySet().stream()
|
||||
.anyMatch(en -> en.getValue().contains("spring-cloud-kubernetes-k8s-client-discovery-server")),
|
||||
"");
|
||||
|
||||
Assertions.assertThat(BASIC_JSON_TESTER.from(result))
|
||||
.<LinkedHashMap<String, String>>extractingJsonPathArrayValue("$.[*]").areAtLeastOne(wireMockService);
|
||||
|
||||
Assertions.assertThat(BASIC_JSON_TESTER.from(result))
|
||||
.<LinkedHashMap<String, String>>extractingJsonPathArrayValue("$.[*]")
|
||||
.areAtLeastOne(discoveryServerService);
|
||||
}
|
||||
|
||||
private static WebClient.Builder builder() {
|
||||
return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create()));
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.springframework.cloud.kubernetes.discoveryclient.it;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.kubernetes.client.openapi.apis.RbacAuthorizationV1Api;
|
||||
import io.kubernetes.client.openapi.models.V1ClusterRoleBinding;
|
||||
@@ -62,6 +63,18 @@ class DiscoveryClientIT {
|
||||
{
|
||||
"name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_0",
|
||||
"value": "left"
|
||||
},
|
||||
{
|
||||
"name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_CATALOGSERVICESWATCHDELAY",
|
||||
"value": "3000"
|
||||
},
|
||||
{
|
||||
"name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_DISCOVERY",
|
||||
"value": "DEBUG"
|
||||
},
|
||||
{
|
||||
"name": "SPRING_CLOUD_KUBERNETES_HTTP_DISCOVERY_CATALOG_WATCHER_ENABLED",
|
||||
"value": "TRUE"
|
||||
}
|
||||
]
|
||||
}]
|
||||
@@ -83,6 +96,18 @@ class DiscoveryClientIT {
|
||||
{
|
||||
"name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_ALL_NAMESPACES",
|
||||
"value": "TRUE"
|
||||
},
|
||||
{
|
||||
"name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_DISCOVERYSERVER",
|
||||
"value": "DEBUG"
|
||||
},
|
||||
{
|
||||
"name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_CATALOGSERVICESWATCHDELAY",
|
||||
"value": "3000"
|
||||
},
|
||||
{
|
||||
"name": "SPRING_CLOUD_KUBERNETES_HTTP_DISCOVERY_CATALOG_WATCHER_ENABLED",
|
||||
"value": "TRUE"
|
||||
}
|
||||
]
|
||||
}]
|
||||
@@ -160,22 +185,22 @@ class DiscoveryClientIT {
|
||||
util.deleteNamespace(NAMESPACE_RIGHT);
|
||||
|
||||
discoveryServer(Phase.DELETE);
|
||||
discoveryIt(Phase.DELETE);
|
||||
discoveryClient(Phase.DELETE);
|
||||
Commons.systemPrune();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDiscoveryClient() {
|
||||
discoveryIt(Phase.CREATE);
|
||||
discoveryClient(Phase.CREATE);
|
||||
testLoadBalancer();
|
||||
testHealth();
|
||||
|
||||
patchForAllNamespaces("docker.io/springcloud/spring-cloud-kubernetes-discoveryserver:" + Commons.pomVersion(),
|
||||
"spring-cloud-kubernetes-discoveryserver-deployment", NAMESPACE);
|
||||
patchForNamespaceFilter(
|
||||
"docker.io/springcloud/spring-cloud-kubernetes-k8s-client-discovery-server:" + Commons.pomVersion(),
|
||||
"spring-cloud-kubernetes-k8s-client-discovery-server-deployment", NAMESPACE);
|
||||
patchForAllNamespaces("docker.io/springcloud/spring-cloud-kubernetes-discoveryserver:" + Commons.pomVersion(),
|
||||
"spring-cloud-kubernetes-discoveryserver-deployment", NAMESPACE);
|
||||
testNamespaceDiscoveryClient();
|
||||
testNamespaceDiscoveryClient(K3S);
|
||||
}
|
||||
|
||||
private void testLoadBalancer() {
|
||||
@@ -187,6 +212,31 @@ class DiscoveryClientIT {
|
||||
|
||||
Assertions.assertThat(BASIC_JSON_TESTER.from(result)).extractingJsonPathArrayValue("$")
|
||||
.contains("spring-cloud-kubernetes-discoveryserver");
|
||||
|
||||
// since 'spring.cloud.kubernetes.http.discovery.client.catalog.watcher.enabled'
|
||||
// is false by default, we will not receive any heartbeat events,
|
||||
// simply because there are no beans registered to provide that to us.
|
||||
// We assert this by doing a call to our internal /state
|
||||
// endpoint, waiting 10 seconds and doing it again. Since the watch delay is set
|
||||
// to 3 seconds, if there would be proper events,
|
||||
// we would get a result that is different from '[]'.
|
||||
|
||||
WebClient.Builder stateBuilder = builder();
|
||||
WebClient client = stateBuilder.baseUrl("http://localhost:80/discoveryclient-it/state").build();
|
||||
String stateResult = client.method(HttpMethod.GET).retrieve().bodyToMono(String.class).retryWhen(retrySpec())
|
||||
.block();
|
||||
Assertions.assertThat(BASIC_JSON_TESTER.from(stateResult)).isEqualTo("[]");
|
||||
|
||||
try {
|
||||
Thread.sleep(TimeUnit.SECONDS.toMillis(10));
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
String stateResultAfter10Seconds = client.method(HttpMethod.GET).retrieve().bodyToMono(String.class)
|
||||
.retryWhen(retrySpec()).block();
|
||||
Assertions.assertThat(BASIC_JSON_TESTER.from(stateResultAfter10Seconds)).isEqualTo("[]");
|
||||
}
|
||||
|
||||
void testHealth() {
|
||||
@@ -200,7 +250,7 @@ class DiscoveryClientIT {
|
||||
.extractingJsonPathStringValue("$.components.discoveryComposite.status").isEqualTo("UP");
|
||||
}
|
||||
|
||||
private static void discoveryIt(Phase phase) {
|
||||
private static void discoveryClient(Phase phase) {
|
||||
V1Deployment deployment = (V1Deployment) util
|
||||
.yaml("client/spring-cloud-kubernetes-discoveryclient-it-deployment.yaml");
|
||||
V1Service service = (V1Service) util.yaml("client/spring-cloud-kubernetes-discoveryclient-it-service.yaml");
|
||||
|
||||
@@ -30,5 +30,8 @@ spec:
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 2
|
||||
failureThreshold: 3
|
||||
env:
|
||||
- name: SPRING_CLOUD_KUBERNETES_DISCOVERY_CATALOGSERVICESWATCHDELAY
|
||||
value: "3000"
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
|
||||
@@ -24,5 +24,10 @@ spec:
|
||||
httpGet:
|
||||
port: 8761
|
||||
path: /actuator/health/liveness
|
||||
env:
|
||||
- name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_DISCOVERYSERVER
|
||||
value: "DEBUG"
|
||||
- name: SPRING_CLOUD_KUBERNETES_DISCOVERY_CATALOGSERVICESWATCHDELAY
|
||||
value: "3000"
|
||||
ports:
|
||||
- containerPort: 8761
|
||||
|
||||
Reference in New Issue
Block a user