Skip to content

Commit 45c932f

Browse files
authored
Merge pull request #48121 from poldinik/feature/47989-discovery-based-config-server-integration
Added support for dynamic discovery of Config Server instances
2 parents aa054b1 + eddb857 commit 45c932f

15 files changed

+567
-57
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package io.quarkus.spring.cloud.config.client.runtime;
2+
3+
import java.net.URI;
4+
5+
public interface ConfigServerBaseUrlProvider {
6+
URI get();
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package io.quarkus.spring.cloud.config.client.runtime;
2+
3+
import java.net.URI;
4+
5+
public record ConfigServerUrl(URI baseURI, int port, String host, String completeURLString) {
6+
7+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package io.quarkus.spring.cloud.config.client.runtime;
2+
3+
import java.net.URI;
4+
import java.net.URISyntaxException;
5+
6+
import org.jboss.logging.Logger;
7+
8+
import io.quarkus.spring.cloud.config.client.runtime.util.UrlUtility;
9+
10+
public class DirectConfigServerBaseUrlProvider implements ConfigServerBaseUrlProvider {
11+
12+
private static final Logger log = Logger.getLogger(DirectConfigServerBaseUrlProvider.class);
13+
private final SpringCloudConfigClientConfig config;
14+
15+
public DirectConfigServerBaseUrlProvider(SpringCloudConfigClientConfig config) {
16+
this.config = config;
17+
}
18+
19+
@Override
20+
public URI get() {
21+
log.info("Getting config server URL with Direct ConfigServer BaseUrl");
22+
String url = config.url();
23+
validate(url);
24+
try {
25+
return UrlUtility.toURI(url);
26+
} catch (URISyntaxException e) {
27+
throw new IllegalArgumentException("Value: '" + config.url()
28+
+ "' of property 'quarkus.spring-cloud-config.url' is invalid", e);
29+
}
30+
}
31+
32+
private void validate(String url) {
33+
if (null == url || url.isEmpty()) {
34+
throw new IllegalArgumentException(
35+
"The 'quarkus.spring-cloud-config.url' property cannot be empty");
36+
}
37+
}
38+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package io.quarkus.spring.cloud.config.client.runtime;
2+
3+
import java.net.URI;
4+
import java.net.URISyntaxException;
5+
6+
import org.jboss.logging.Logger;
7+
8+
import io.quarkus.spring.cloud.config.client.runtime.eureka.DiscoveryService;
9+
import io.quarkus.spring.cloud.config.client.runtime.util.UrlUtility;
10+
11+
public class DiscoveryConfigServerBaseUrlProvider implements ConfigServerBaseUrlProvider {
12+
13+
private static final Logger log = Logger.getLogger(DiscoveryConfigServerBaseUrlProvider.class);
14+
private final DiscoveryService discoveryService;
15+
private final SpringCloudConfigClientConfig config;
16+
17+
public DiscoveryConfigServerBaseUrlProvider(DiscoveryService discoveryService, SpringCloudConfigClientConfig config) {
18+
this.discoveryService = discoveryService;
19+
this.config = config;
20+
}
21+
22+
@Override
23+
public URI get() {
24+
log.info("Getting config server URL with Discovery ConfigServer");
25+
try {
26+
return UrlUtility.toURI(discoveryService.discover(config));
27+
} catch (URISyntaxException e) {
28+
throw new RuntimeException(e);
29+
}
30+
}
31+
32+
}

extensions/spring-cloud-config-client/runtime/src/main/java/io/quarkus/spring/cloud/config/client/runtime/SpringCloudConfigClientConfig.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,43 @@ public interface SpringCloudConfigClientConfig {
129129
@WithDefault("450")
130130
int ordinal();
131131

132+
/**
133+
* Configuration for Config Server discovery.
134+
*/
135+
Optional<DiscoveryConfig> discovery();
136+
137+
interface DiscoveryConfig {
138+
/**
139+
* Enable discovery of the Spring Cloud Config Server
140+
*/
141+
@WithDefault("false")
142+
boolean enabled();
143+
144+
/**
145+
* The service ID to use when discovering the Spring Cloud Config Server
146+
*/
147+
Optional<String> serviceId();
148+
149+
/**
150+
* Eureka server configuration
151+
*/
152+
Optional<EurekaConfig> eurekaConfig();
153+
154+
interface EurekaConfig {
155+
/**
156+
* The service URL to use to specify Eureka server
157+
*/
158+
Map<String, String> serviceUrl();
159+
160+
/**
161+
* Indicates how often(in seconds) to fetch the registry information from the eureka server.
162+
*/
163+
@WithDefault("30S")
164+
@WithConverter(DurationConverter.class)
165+
Duration registryFetchIntervalSeconds();
166+
}
167+
}
168+
132169
/** */
133170
default boolean usernameAndPasswordSet() {
134171
return username().isPresent() && password().isPresent();

extensions/spring-cloud-config-client/runtime/src/main/java/io/quarkus/spring/cloud/config/client/runtime/VertxSpringCloudConfigGateway.java

Lines changed: 47 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
import java.io.InputStream;
66
import java.net.URI;
7-
import java.net.URISyntaxException;
87
import java.nio.file.Files;
98
import java.nio.file.Path;
9+
import java.time.Duration;
1010
import java.util.ArrayList;
1111
import java.util.List;
1212
import java.util.Map;
@@ -20,6 +20,11 @@
2020

2121
import io.quarkus.runtime.ResettableSystemProperties;
2222
import io.quarkus.runtime.util.ClassPathUtils;
23+
import io.quarkus.spring.cloud.config.client.runtime.eureka.DiscoveryService;
24+
import io.quarkus.spring.cloud.config.client.runtime.eureka.EurekaClient;
25+
import io.quarkus.spring.cloud.config.client.runtime.eureka.EurekaResponseMapper;
26+
import io.quarkus.spring.cloud.config.client.runtime.eureka.RandomEurekaInstanceSelector;
27+
import io.quarkus.spring.cloud.config.client.runtime.util.UrlUtility;
2328
import io.smallrye.mutiny.Uni;
2429
import io.vertx.core.VertxOptions;
2530
import io.vertx.core.net.JksOptions;
@@ -44,18 +49,41 @@ public class VertxSpringCloudConfigGateway implements SpringCloudConfigClientGat
4449
private final SpringCloudConfigClientConfig config;
4550
private final Vertx vertx;
4651
private final WebClient webClient;
47-
private final URI baseURI;
52+
private final ConfigServerBaseUrlProvider configServerBaseUrlProvider;
4853

4954
public VertxSpringCloudConfigGateway(SpringCloudConfigClientConfig config) {
5055
this.config = config;
51-
try {
52-
this.baseURI = determineBaseUri(config);
53-
} catch (URISyntaxException e) {
54-
throw new IllegalArgumentException("Value: '" + config.url()
55-
+ "' of property 'quarkus.spring-cloud-config.url' is invalid", e);
56-
}
5756
this.vertx = createVertxInstance();
5857
this.webClient = createHttpClient(vertx, config);
58+
this.configServerBaseUrlProvider = createConfigServerProvider(config);
59+
}
60+
61+
private ConfigServerBaseUrlProvider createConfigServerProvider(SpringCloudConfigClientConfig config) {
62+
if (!config.discovery().isPresent() || (!config.discovery().get().enabled())) {
63+
return new DirectConfigServerBaseUrlProvider(config);
64+
}
65+
DiscoveryService discoveryService = createDiscoveryService(config.discovery().get());
66+
return new DiscoveryConfigServerBaseUrlProvider(discoveryService, config);
67+
}
68+
69+
private DiscoveryService createDiscoveryService(SpringCloudConfigClientConfig.DiscoveryConfig config) {
70+
EurekaClient eurekaClient = createEurekaClient(config.eurekaConfig().get());
71+
return new DiscoveryService(eurekaClient);
72+
}
73+
74+
private EurekaClient createEurekaClient(SpringCloudConfigClientConfig.DiscoveryConfig.EurekaConfig config) {
75+
if (config == null) {
76+
throw new IllegalArgumentException("Eureka configuration is required");
77+
}
78+
Duration fetchInterval = config.registryFetchIntervalSeconds();
79+
EurekaResponseMapper responseMapper = new EurekaResponseMapper();
80+
RandomEurekaInstanceSelector instanceSelector = new RandomEurekaInstanceSelector();
81+
82+
return new EurekaClient(
83+
webClient,
84+
fetchInterval,
85+
responseMapper,
86+
instanceSelector);
5987
}
6088

6189
private Vertx createVertxInstance() {
@@ -156,54 +184,43 @@ private static byte[] allBytes(InputStream inputStream) throws Exception {
156184
return inputStream.readAllBytes();
157185
}
158186

159-
private URI determineBaseUri(SpringCloudConfigClientConfig springCloudConfigClientConfig) throws URISyntaxException {
160-
String url = springCloudConfigClientConfig.url();
161-
if (null == url || url.isEmpty()) {
162-
throw new IllegalArgumentException(
163-
"The 'quarkus.spring-cloud-config.url' property cannot be empty");
164-
}
165-
if (url.endsWith("/")) {
166-
return new URI(url.substring(0, url.length() - 1));
167-
}
168-
return new URI(url);
169-
}
170-
171-
private String finalURI(String applicationName, String profile) {
187+
private ConfigServerUrl toConfigServerUrl(String applicationName, String profile) {
188+
URI baseURI = configServerBaseUrlProvider.get();
172189
String path = baseURI.getPath();
173-
List<String> finalPathSegments = new ArrayList<String>();
190+
List<String> finalPathSegments = new ArrayList<>();
174191
finalPathSegments.add(path);
175192
finalPathSegments.add(applicationName);
176193
finalPathSegments.add(profile);
177194
if (config.label().isPresent()) {
178195
finalPathSegments.add(config.label().get());
179196
}
180-
return String.join("/", finalPathSegments);
197+
return new ConfigServerUrl(baseURI, UrlUtility.getPort(baseURI), baseURI.getHost(),
198+
String.join("/", finalPathSegments));
181199
}
182200

183201
@Override
184202
public Uni<Response> exchange(String applicationName, String profile) {
185-
final String requestURI = finalURI(applicationName, profile);
186-
String finalURI = getFinalURI(applicationName, profile);
203+
final ConfigServerUrl requestURI = toConfigServerUrl(applicationName, profile);
187204
HttpRequest<Buffer> request = webClient
188-
.get(getPort(baseURI), baseURI.getHost(), requestURI)
189-
.ssl(isHttps(baseURI))
205+
.get(requestURI.port(), requestURI.host(), requestURI.completeURLString())
206+
.ssl(UrlUtility.isHttps(requestURI.baseURI()))
190207
.putHeader("Accept", "application/json");
191208
if (config.usernameAndPasswordSet()) {
192209
request.basicAuthentication(config.username().get(), config.password().get());
193210
}
194211
for (Map.Entry<String, String> entry : config.headers().entrySet()) {
195212
request.putHeader(entry.getKey(), entry.getValue());
196213
}
197-
log.debug("Attempting to read configuration from '" + finalURI + "'.");
214+
log.debug("Attempting to read configuration from '" + requestURI.completeURLString() + "'.");
198215
return request.send().map(r -> {
199216
log.debug("Received HTTP response code '" + r.statusCode() + "'");
200217
if (r.statusCode() != 200) {
201218
throw new RuntimeException("Got unexpected HTTP response code " + r.statusCode()
202-
+ " from " + finalURI);
219+
+ " from " + requestURI.completeURLString());
203220
} else {
204221
String bodyAsString = r.bodyAsString();
205222
if (bodyAsString.isEmpty()) {
206-
throw new RuntimeException("Got empty HTTP response body " + finalURI);
223+
throw new RuntimeException("Got empty HTTP response body " + requestURI.completeURLString());
207224
}
208225
try {
209226
log.debug("Attempting to deserialize response");
@@ -215,22 +232,6 @@ public Uni<Response> exchange(String applicationName, String profile) {
215232
});
216233
}
217234

218-
private boolean isHttps(URI uri) {
219-
return uri.getScheme().contains("https");
220-
}
221-
222-
private int getPort(URI uri) {
223-
return uri.getPort() != -1 ? uri.getPort() : (isHttps(uri) ? 443 : 80);
224-
}
225-
226-
private String getFinalURI(String applicationName, String profile) {
227-
String finalURI = baseURI.toString() + "/" + applicationName + "/" + profile;
228-
if (config.label().isPresent()) {
229-
finalURI += "/" + config.label().get();
230-
}
231-
return finalURI;
232-
}
233-
234235
@Override
235236
public void close() {
236237
this.webClient.close();
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package io.quarkus.spring.cloud.config.client.runtime.eureka;
2+
3+
import static java.util.stream.Collectors.toMap;
4+
5+
import java.util.Map;
6+
7+
import org.jboss.logging.Logger;
8+
9+
import io.quarkus.spring.cloud.config.client.runtime.SpringCloudConfigClientConfig;
10+
import io.vertx.core.json.JsonObject;
11+
12+
public class DiscoveryService {
13+
14+
private static final Logger log = Logger.getLogger(DiscoveryService.class);
15+
private static final String DEFAULT_ZONE = "defaultZone";
16+
17+
private final EurekaClient eurekaClient;
18+
19+
public DiscoveryService(EurekaClient eurekaClient) {
20+
this.eurekaClient = eurekaClient;
21+
}
22+
23+
public String discover(SpringCloudConfigClientConfig config) {
24+
SpringCloudConfigClientConfig.DiscoveryConfig discoveryConfig = config.discovery().get();
25+
validate(discoveryConfig);
26+
27+
String serviceId = discoveryConfig.serviceId().get();
28+
SpringCloudConfigClientConfig.DiscoveryConfig.EurekaConfig eurekaConfig = discoveryConfig.eurekaConfig().get();
29+
String defaultServiceUrl = eurekaConfig.serviceUrl().get(DEFAULT_ZONE);
30+
31+
Map<String, String> serviceUrlMap = eurekaConfig
32+
.serviceUrl()
33+
.entrySet()
34+
.stream().filter(entry -> !DEFAULT_ZONE.equals(entry.getKey()))
35+
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
36+
37+
log.debug("Attempting to discover Spring Cloud Config Server URL for service '" + serviceId
38+
+ "' using the following URLs: " + serviceUrlMap.values());
39+
for (Map.Entry<String, String> entry : serviceUrlMap.entrySet()) {
40+
try {
41+
return getHomeUrl(entry.getValue(), serviceId);
42+
} catch (Exception e) {
43+
log.debug("Timed out while waiting for Spring Cloud Config Server URL for service '" + serviceId + "'", e);
44+
}
45+
}
46+
47+
log.debug("Fallback Attempting to discover Spring Cloud Config Server URL for service '" + serviceId
48+
+ "' using the default URL: " + defaultServiceUrl);
49+
try {
50+
return getHomeUrl(defaultServiceUrl, serviceId);
51+
} catch (Exception e) {
52+
log.debug("Timed out while waiting for Spring Cloud Config Server URL for service '" + serviceId + "'", e);
53+
}
54+
55+
throw new RuntimeException("Unable to discover Spring Cloud Config Server URL for service '" + serviceId + "'");
56+
}
57+
58+
private void validate(SpringCloudConfigClientConfig.DiscoveryConfig discoveryConfig) {
59+
if (discoveryConfig.eurekaConfig().isEmpty()) {
60+
throw new IllegalArgumentException("No Eureka configuration has been provided");
61+
}
62+
if (discoveryConfig.eurekaConfig().get().serviceUrl().isEmpty()) {
63+
throw new IllegalArgumentException("No service URLs have been configured for service");
64+
}
65+
if (discoveryConfig.serviceId().isEmpty()) {
66+
throw new IllegalArgumentException("No service ID has been configured for service");
67+
}
68+
}
69+
70+
private String getHomeUrl(String defaultServiceUrl, String serviceId) {
71+
JsonObject instance = eurekaClient.fetchInstances(defaultServiceUrl, serviceId);
72+
return instance.getString("homePageUrl");
73+
}
74+
75+
}

0 commit comments

Comments
 (0)