Skip to content

Commit 47dfd25

Browse files
committed
Implement new API in ConfigServerInstanceProvider.Function
1 parent 4c4e012 commit 47dfd25

File tree

6 files changed

+247
-27
lines changed

6 files changed

+247
-27
lines changed

pom.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
<spring-cloud-openfeign.version>3.1.8-SNAPSHOT</spring-cloud-openfeign.version>
2828
<spring-cloud-stream.version>3.2.9-SNAPSHOT</spring-cloud-stream.version>
2929
<testcontainers.version>1.17.6</testcontainers.version>
30+
<mockserverclient.version>5.15.0</mockserverclient.version>
3031
</properties>
3132

3233
<scm>
@@ -156,11 +157,21 @@
156157
<artifactId>consul</artifactId>
157158
<version>${testcontainers.version}</version>
158159
</dependency>
160+
<dependency>
161+
<groupId>org.testcontainers</groupId>
162+
<artifactId>mockserver</artifactId>
163+
<version>${testcontainers.version}</version>
164+
</dependency>
159165
<dependency>
160166
<groupId>org.testcontainers</groupId>
161167
<artifactId>junit-jupiter</artifactId>
162168
<version>${testcontainers.version}</version>
163169
</dependency>
170+
<dependency>
171+
<groupId>org.mock-server</groupId>
172+
<artifactId>mockserver-client-java</artifactId>
173+
<version>${mockserverclient.version}</version>
174+
</dependency>
164175
</dependencies>
165176
</dependencyManagement>
166177

spring-cloud-consul-discovery/pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,16 @@
134134
<artifactId>consul</artifactId>
135135
<scope>test</scope>
136136
</dependency>
137+
<dependency>
138+
<groupId>org.testcontainers</groupId>
139+
<artifactId>mockserver</artifactId>
140+
<scope>test</scope>
141+
</dependency>
142+
<dependency>
143+
<groupId>org.mock-server</groupId>
144+
<artifactId>mockserver-client-java</artifactId>
145+
<scope>test</scope>
146+
</dependency>
137147
<dependency>
138148
<groupId>org.testcontainers</groupId>
139149
<artifactId>junit-jupiter</artifactId>

spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/configclient/ConsulConfigServerBootstrapper.java

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,18 @@
1717
package org.springframework.cloud.consul.discovery.configclient;
1818

1919
import java.util.Collections;
20+
import java.util.List;
2021

2122
import com.ecwid.consul.v1.ConsulClient;
23+
import org.apache.commons.logging.Log;
2224

25+
import org.springframework.boot.BootstrapContext;
2326
import org.springframework.boot.BootstrapRegistry;
2427
import org.springframework.boot.BootstrapRegistryInitializer;
2528
import org.springframework.boot.context.properties.bind.BindHandler;
2629
import org.springframework.boot.context.properties.bind.Bindable;
2730
import org.springframework.boot.context.properties.bind.Binder;
31+
import org.springframework.cloud.client.ServiceInstance;
2832
import org.springframework.cloud.commons.util.InetUtils;
2933
import org.springframework.cloud.commons.util.InetUtilsProperties;
3034
import org.springframework.cloud.config.client.ConfigClientProperties;
@@ -84,24 +88,67 @@ public void initialize(BootstrapRegistry registry) {
8488
discoveryClient);
8589
}
8690
});
87-
registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, context -> {
88-
if (!isDiscoveryEnabled(context.get(Binder.class))) {
89-
return (id) -> Collections.emptyList();
90-
}
91-
ConsulDiscoveryClient discoveryClient = context.get(ConsulDiscoveryClient.class);
92-
return discoveryClient::getInstances;
93-
});
9491

92+
// We need to pass the lambda here so we do not create a new instance of
93+
// ConfigServerInstanceProvider.Function
94+
// which would result in a ClassNotFoundException when Spring Cloud Config is not
95+
// on the classpath
96+
registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, ConsulFunction::new);
9597
}
9698

9799
private BindHandler getBindHandler(org.springframework.boot.BootstrapContext context) {
98100
return context.getOrElse(BindHandler.class, null);
99101
}
100102

101-
private boolean isDiscoveryEnabled(Binder binder) {
103+
private static boolean isDiscoveryEnabled(Binder binder) {
102104
return binder.bind(ConfigClientProperties.CONFIG_DISCOVERY_ENABLED, Boolean.class).orElse(false)
103105
&& binder.bind(ConditionalOnConsulDiscoveryEnabled.PROPERTY, Boolean.class).orElse(true)
104106
&& binder.bind("spring.cloud.discovery.enabled", Boolean.class).orElse(true);
105107
}
106108

109+
/*
110+
* This Function is executed when loading config data. Because of this we cannot rely
111+
* on the BootstrapContext because Boot has not finished loading all the configuration
112+
* data so if we ask the BootstrapContext for configuration data it will not have it.
113+
* The apply method in this function is passed the Binder and BindHandler from the
114+
* config data context which has the configuration properties that have been loaded so
115+
* far in the config data process.
116+
*
117+
* We will create many of the same beans in this function as we do above in the
118+
* initializer above. We do both to maintain compatibility since we are promoting
119+
* those beans to the main application context.
120+
*/
121+
static final class ConsulFunction implements ConfigServerInstanceProvider.Function {
122+
123+
private final BootstrapContext context;
124+
125+
private ConsulFunction(BootstrapContext context) {
126+
this.context = context;
127+
}
128+
129+
@Override
130+
public List<ServiceInstance> apply(String serviceId) {
131+
return apply(serviceId, null, null, null);
132+
}
133+
134+
@Override
135+
public List<ServiceInstance> apply(String serviceId, Binder binder, BindHandler bindHandler, Log log) {
136+
if (binder == null || !isDiscoveryEnabled(binder)) {
137+
return Collections.emptyList();
138+
}
139+
140+
ConsulProperties consulProperties = binder
141+
.bind(ConsulProperties.PREFIX, Bindable.of(ConsulProperties.class), bindHandler)
142+
.orElseGet(ConsulProperties::new);
143+
ConsulClient consulClient = ConsulAutoConfiguration.createConsulClient(consulProperties);
144+
ConsulDiscoveryProperties properties = binder
145+
.bind(ConsulDiscoveryProperties.PREFIX, Bindable.of(ConsulDiscoveryProperties.class), bindHandler)
146+
.orElseGet(() -> new ConsulDiscoveryProperties(new InetUtils(new InetUtilsProperties())));
147+
ConsulDiscoveryClient discoveryClient = new ConsulDiscoveryClient(consulClient, properties);
148+
149+
return discoveryClient.getInstances(serviceId);
150+
}
151+
152+
}
153+
107154
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.consul.discovery.configclient;
18+
19+
import java.util.Arrays;
20+
import java.util.HashMap;
21+
import java.util.LinkedHashSet;
22+
import java.util.Map;
23+
import java.util.Set;
24+
25+
import com.ecwid.consul.v1.ConsulClient;
26+
import com.ecwid.consul.v1.agent.model.NewService;
27+
import com.fasterxml.jackson.core.JsonProcessingException;
28+
import com.fasterxml.jackson.databind.ObjectMapper;
29+
import org.junit.jupiter.api.AfterEach;
30+
import org.junit.jupiter.api.BeforeEach;
31+
import org.junit.jupiter.api.Test;
32+
import org.mockserver.client.MockServerClient;
33+
import org.testcontainers.consul.ConsulContainer;
34+
import org.testcontainers.containers.MockServerContainer;
35+
import org.testcontainers.junit.jupiter.Container;
36+
import org.testcontainers.junit.jupiter.Testcontainers;
37+
import org.testcontainers.utility.DockerImageName;
38+
39+
import org.springframework.boot.SpringBootConfiguration;
40+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
41+
import org.springframework.boot.builder.SpringApplicationBuilder;
42+
import org.springframework.cloud.config.environment.Environment;
43+
import org.springframework.cloud.config.environment.PropertySource;
44+
import org.springframework.cloud.consul.ConsulAutoConfiguration;
45+
import org.springframework.cloud.consul.ConsulProperties;
46+
import org.springframework.cloud.consul.test.ConsulTestcontainers;
47+
import org.springframework.context.ConfigurableApplicationContext;
48+
49+
import static org.assertj.core.api.Assertions.assertThat;
50+
import static org.mockserver.model.HttpRequest.request;
51+
import static org.mockserver.model.HttpResponse.response;
52+
53+
/**
54+
* @author Ryan Baxter
55+
*/
56+
57+
@Testcontainers
58+
public class ConsulConfigServerBootstrapperIT {
59+
60+
public static final DockerImageName MOCKSERVER_IMAGE = DockerImageName.parse("mockserver/mockserver")
61+
.withTag("mockserver-" + MockServerClient.class.getPackage().getImplementationVersion());
62+
63+
@Container
64+
static ConsulContainer consul = ConsulTestcontainers.createConsulContainer("1.10");
65+
66+
@Container
67+
static MockServerContainer mockServer = new MockServerContainer(MOCKSERVER_IMAGE);
68+
69+
private ConfigurableApplicationContext context;
70+
71+
@BeforeEach
72+
void before() {
73+
ConsulProperties consulProperties = new ConsulProperties();
74+
consulProperties.setHost(consul.getHost());
75+
consulProperties.setPort(consul.getMappedPort(ConsulTestcontainers.DEFAULT_PORT));
76+
ConsulClient client = ConsulAutoConfiguration.createConsulClient(consulProperties);
77+
NewService newService = new NewService();
78+
newService.setId("consul-configserver");
79+
newService.setName("consul-configserver");
80+
newService.setAddress(mockServer.getHost());
81+
newService.setPort(mockServer.getServerPort());
82+
client.agentServiceRegister(newService);
83+
84+
}
85+
86+
@AfterEach
87+
void after() {
88+
this.context.close();
89+
}
90+
91+
@Test
92+
public void contextLoads() throws JsonProcessingException {
93+
Environment environment = new Environment("test", "default");
94+
Map<String, Object> properties = new HashMap<>();
95+
properties.put("hello", "world");
96+
PropertySource p = new PropertySource("p1", properties);
97+
environment.add(p);
98+
ObjectMapper objectMapper = new ObjectMapper();
99+
try (MockServerClient mockServerClient = new MockServerClient(mockServer.getHost(),
100+
mockServer.getMappedPort(MockServerContainer.PORT))) {
101+
mockServerClient.when(request().withPath("/application/default"))
102+
.respond(response().withBody(objectMapper.writeValueAsString(environment))
103+
.withHeader("content-type", "application/json"));
104+
this.context = setup().run();
105+
assertThat(this.context.getEnvironment().getProperty("hello")).isEqualTo("world");
106+
}
107+
108+
}
109+
110+
SpringApplicationBuilder setup(String... env) {
111+
SpringApplicationBuilder builder = new SpringApplicationBuilder(TestConfig.class)
112+
.properties(addDefaultEnv(env));
113+
return builder;
114+
}
115+
116+
private String[] addDefaultEnv(String[] env) {
117+
Set<String> set = new LinkedHashSet<>();
118+
if (env != null && env.length > 0) {
119+
set.addAll(Arrays.asList(env));
120+
}
121+
set.add("spring.config.import=classpath:bootstrapper.yaml");
122+
set.add("spring.cloud.config.enabled=true");
123+
set.add("spring.cloud.service-registry.auto-registration.enabled=false");
124+
set.add(ConsulProperties.PREFIX + ".host=" + consul.getHost());
125+
set.add(ConsulProperties.PREFIX + ".port=" + consul.getMappedPort(ConsulTestcontainers.DEFAULT_PORT));
126+
return set.toArray(new String[0]);
127+
}
128+
129+
@SpringBootConfiguration
130+
@EnableAutoConfiguration
131+
static class TestConfig {
132+
133+
}
134+
135+
}

spring-cloud-consul-discovery/src/test/java/org/springframework/cloud/consul/discovery/configclient/ConsulConfigServerBootstrapperTests.java

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.concurrent.atomic.AtomicReference;
2121

2222
import com.ecwid.consul.transport.TransportException;
23+
import org.apache.commons.logging.Log;
2324
import org.junit.jupiter.api.Test;
2425

2526
import org.springframework.boot.BootstrapRegistry;
@@ -30,13 +31,15 @@
3031
import org.springframework.boot.context.properties.bind.BindContext;
3132
import org.springframework.boot.context.properties.bind.BindHandler;
3233
import org.springframework.boot.context.properties.bind.Bindable;
34+
import org.springframework.boot.context.properties.bind.Binder;
3335
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
3436
import org.springframework.cloud.config.client.ConfigServerInstanceProvider;
3537
import org.springframework.cloud.consul.discovery.ConsulDiscoveryClient;
3638
import org.springframework.context.ConfigurableApplicationContext;
3739

3840
import static org.assertj.core.api.Assertions.assertThat;
3941
import static org.assertj.core.api.Assertions.assertThatThrownBy;
42+
import static org.mockito.Mockito.mock;
4043

4144
public class ConsulConfigServerBootstrapperTests {
4245

@@ -47,37 +50,41 @@ public void notEnabledDoesNotAddInstanceProviderFn() {
4750
.addBootstrapRegistryInitializer(registry -> registry.addCloseListener(event -> {
4851
ConfigServerInstanceProvider.Function providerFn = event.getBootstrapContext()
4952
.get(ConfigServerInstanceProvider.Function.class);
50-
assertThat(providerFn.apply("id"))
51-
.as("ConfigServerInstanceProvider.Function should return empty list")
52-
.isEqualTo(Collections.EMPTY_LIST);
53+
Log log = mock(Log.class);
54+
assertThat(providerFn.apply("id", event.getBootstrapContext().get(Binder.class),
55+
event.getBootstrapContext().get(BindHandler.class), log))
56+
.as("ConfigServerInstanceProvider.Function should return empty list")
57+
.isEqualTo(Collections.EMPTY_LIST);
5358
})).run().close();
5459
}
5560

5661
@Test
5762
public void consulDiscoveryClientNotEnabledProvidesEmptyList() {
5863
new SpringApplicationBuilder(TestConfig.class)
59-
.properties("--server.port=0", "spring.cloud.service-registry.auto-registration.enabled=false",
60-
"spring.cloud.config.discovery.enabled=true", "spring.cloud.consul.discovery.enabled=false")
64+
.properties("--server.port=0", "spring.cloud.service-registry.auto-registration.enabled=false")
6165
.addBootstrapRegistryInitializer(registry -> registry.addCloseListener(event -> {
6266
ConfigServerInstanceProvider.Function providerFn = event.getBootstrapContext()
6367
.get(ConfigServerInstanceProvider.Function.class);
64-
assertThat(providerFn.apply("id"))
65-
.as("ConfigServerInstanceProvider.Function should return empty list")
66-
.isEqualTo(Collections.EMPTY_LIST);
68+
Log log = mock(Log.class);
69+
assertThat(providerFn.apply("id", event.getBootstrapContext().get(Binder.class),
70+
event.getBootstrapContext().get(BindHandler.class), log))
71+
.as("ConfigServerInstanceProvider.Function should return empty list")
72+
.isEqualTo(Collections.EMPTY_LIST);
6773
})).run().close();
6874
}
6975

7076
@Test
7177
public void springCloudDiscoveryClientNotEnabledProvidesEmptyList() {
7278
new SpringApplicationBuilder(TestConfig.class)
73-
.properties("--server.port=0", "spring.cloud.service-registry.auto-registration.enabled=false",
74-
"spring.cloud.config.discovery.enabled=true", "spring.cloud.discovery.enabled=false")
79+
.properties("--server.port=0", "spring.cloud.service-registry.auto-registration.enabled=false")
7580
.addBootstrapRegistryInitializer(registry -> registry.addCloseListener(event -> {
7681
ConfigServerInstanceProvider.Function providerFn = event.getBootstrapContext()
7782
.get(ConfigServerInstanceProvider.Function.class);
78-
assertThat(providerFn.apply("id"))
79-
.as("ConfigServerInstanceProvider.Function should return empty list")
80-
.isEqualTo(Collections.EMPTY_LIST);
83+
Log log = mock(Log.class);
84+
assertThat(providerFn.apply("id", event.getBootstrapContext().get(Binder.class),
85+
event.getBootstrapContext().get(BindHandler.class), log))
86+
.as("ConfigServerInstanceProvider.Function should return empty list")
87+
.isEqualTo(Collections.EMPTY_LIST);
8188
})).run().close();
8289
}
8390

@@ -88,17 +95,19 @@ public void enabledAddsInstanceProviderFn() {
8895
ConfigurableApplicationContext context = new SpringApplicationBuilder(TestConfig.class)
8996
.properties("--server.port=0", "spring.cloud.config.discovery.enabled=true",
9097
"spring.cloud.consul.discovery.hostname=myhost",
91-
"spring.cloud.service-registry.auto-registration.enabled=false",
92-
"spring.cloud.consul.host=localhost")
98+
"spring.cloud.service-registry.auto-registration.enabled=false")
9399
.addBootstrapRegistryInitializer(bindHandlerBootstrapper)
94100
.addBootstrapRegistryInitializer(registry -> registry.addCloseListener(event -> {
95101
bootstrapDiscoveryClient.set(event.getBootstrapContext().get(ConsulDiscoveryClient.class));
96102
ConfigServerInstanceProvider.Function providerFn = event.getBootstrapContext()
97103
.get(ConfigServerInstanceProvider.Function.class);
98-
assertThatThrownBy(() -> providerFn.apply("id")).isInstanceOf(TransportException.class)
99-
.hasMessageContaining(
100-
"org.apache.http.conn.HttpHostConnectException: Connect to localhost:8500")
101-
.as("Should have tried to reach out to Consul to get config server instance").isNotNull();
104+
assertThatThrownBy(() -> providerFn.apply("id", event.getBootstrapContext().get(Binder.class),
105+
event.getBootstrapContext().get(BindHandler.class), mock(Log.class)))
106+
.isInstanceOf(TransportException.class)
107+
.hasMessageContaining(
108+
"org.apache.http.conn.HttpHostConnectException: Connect to localhost:8500")
109+
.as("Should have tried to reach out to Consul to get config server instance")
110+
.isNotNull();
102111
})).run();
103112
ConsulDiscoveryClient discoveryClient = context.getBean(ConsulDiscoveryClient.class);
104113
assertThat(discoveryClient == bootstrapDiscoveryClient.get()).isTrue();
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
spring:
2+
config:
3+
import: "optional:configserver:"
4+
cloud:
5+
config:
6+
discovery:
7+
service-id: consul-configserver
8+
enabled: true

0 commit comments

Comments
 (0)