Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.quarkus.spring.cloud.config.client.runtime;

import java.net.URI;

public interface ConfigServerBaseUrlProvider {
URI get();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.quarkus.spring.cloud.config.client.runtime;

import java.net.URI;

public record ConfigServerUrl(URI baseURI, int port, String host, String completeURLString) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.quarkus.spring.cloud.config.client.runtime;

import java.net.URI;
import java.net.URISyntaxException;

import org.jboss.logging.Logger;

import io.quarkus.spring.cloud.config.client.runtime.util.UrlUtility;

public class DirectConfigServerBaseUrlProvider implements ConfigServerBaseUrlProvider {

private static final Logger log = Logger.getLogger(DirectConfigServerBaseUrlProvider.class);
private final SpringCloudConfigClientConfig config;

public DirectConfigServerBaseUrlProvider(SpringCloudConfigClientConfig config) {
this.config = config;
}

@Override
public URI get() {
log.info("Getting config server URL with Direct ConfigServer BaseUrl");
String url = config.url();
validate(url);
try {
return UrlUtility.toURI(url);
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Value: '" + config.url()
+ "' of property 'quarkus.spring-cloud-config.url' is invalid", e);
}
}

private void validate(String url) {
if (null == url || url.isEmpty()) {
throw new IllegalArgumentException(
"The 'quarkus.spring-cloud-config.url' property cannot be empty");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.quarkus.spring.cloud.config.client.runtime;

import java.net.URI;
import java.net.URISyntaxException;

import org.jboss.logging.Logger;

import io.quarkus.spring.cloud.config.client.runtime.eureka.DiscoveryService;
import io.quarkus.spring.cloud.config.client.runtime.util.UrlUtility;

public class DiscoveryConfigServerBaseUrlProvider implements ConfigServerBaseUrlProvider {

private static final Logger log = Logger.getLogger(DiscoveryConfigServerBaseUrlProvider.class);
private final DiscoveryService discoveryService;
private final SpringCloudConfigClientConfig config;

public DiscoveryConfigServerBaseUrlProvider(DiscoveryService discoveryService, SpringCloudConfigClientConfig config) {
this.discoveryService = discoveryService;
this.config = config;
}

@Override
public URI get() {
log.info("Getting config server URL with Discovery ConfigServer");
try {
return UrlUtility.toURI(discoveryService.discover(config));
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,43 @@ public interface SpringCloudConfigClientConfig {
@WithDefault("450")
int ordinal();

/**
* Configuration for Config Server discovery.
*/
Optional<DiscoveryConfig> discovery();

interface DiscoveryConfig {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we should have another layer under DiscoveryConfig named EurekaConfig. The idea here being that in the future we might have some other way to do discovery (Stork for example).

/**
* Enable discovery of the Spring Cloud Config Server
*/
@WithDefault("false")
boolean enabled();

/**
* The service ID to use when discovering the Spring Cloud Config Server
*/
Optional<String> serviceId();

/**
* Eureka server configuration
*/
Optional<EurekaConfig> eurekaConfig();

interface EurekaConfig {
/**
* The service URL to use to specify Eureka server
*/
Map<String, String> serviceUrl();

/**
* Indicates how often(in seconds) to fetch the registry information from the eureka server.
*/
@WithDefault("30S")
@WithConverter(DurationConverter.class)
Duration registryFetchIntervalSeconds();
}
}

/** */
default boolean usernameAndPasswordSet() {
return username().isPresent() && password().isPresent();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
Expand All @@ -20,6 +20,11 @@

import io.quarkus.runtime.ResettableSystemProperties;
import io.quarkus.runtime.util.ClassPathUtils;
import io.quarkus.spring.cloud.config.client.runtime.eureka.DiscoveryService;
import io.quarkus.spring.cloud.config.client.runtime.eureka.EurekaClient;
import io.quarkus.spring.cloud.config.client.runtime.eureka.EurekaResponseMapper;
import io.quarkus.spring.cloud.config.client.runtime.eureka.RandomEurekaInstanceSelector;
import io.quarkus.spring.cloud.config.client.runtime.util.UrlUtility;
import io.smallrye.mutiny.Uni;
import io.vertx.core.VertxOptions;
import io.vertx.core.net.JksOptions;
Expand All @@ -44,18 +49,41 @@ public class VertxSpringCloudConfigGateway implements SpringCloudConfigClientGat
private final SpringCloudConfigClientConfig config;
private final Vertx vertx;
private final WebClient webClient;
private final URI baseURI;
private final ConfigServerBaseUrlProvider configServerBaseUrlProvider;

public VertxSpringCloudConfigGateway(SpringCloudConfigClientConfig config) {
this.config = config;
try {
this.baseURI = determineBaseUri(config);
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Value: '" + config.url()
+ "' of property 'quarkus.spring-cloud-config.url' is invalid", e);
}
this.vertx = createVertxInstance();
this.webClient = createHttpClient(vertx, config);
this.configServerBaseUrlProvider = createConfigServerProvider(config);
}

private ConfigServerBaseUrlProvider createConfigServerProvider(SpringCloudConfigClientConfig config) {
if (!config.discovery().isPresent() || (!config.discovery().get().enabled())) {
return new DirectConfigServerBaseUrlProvider(config);
}
DiscoveryService discoveryService = createDiscoveryService(config.discovery().get());
return new DiscoveryConfigServerBaseUrlProvider(discoveryService, config);
}

private DiscoveryService createDiscoveryService(SpringCloudConfigClientConfig.DiscoveryConfig config) {
EurekaClient eurekaClient = createEurekaClient(config.eurekaConfig().get());
return new DiscoveryService(eurekaClient);
}

private EurekaClient createEurekaClient(SpringCloudConfigClientConfig.DiscoveryConfig.EurekaConfig config) {
if (config == null) {
throw new IllegalArgumentException("Eureka configuration is required");
}
Duration fetchInterval = config.registryFetchIntervalSeconds();
EurekaResponseMapper responseMapper = new EurekaResponseMapper();
RandomEurekaInstanceSelector instanceSelector = new RandomEurekaInstanceSelector();

return new EurekaClient(
webClient,
fetchInterval,
responseMapper,
instanceSelector);
}

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

private URI determineBaseUri(SpringCloudConfigClientConfig springCloudConfigClientConfig) throws URISyntaxException {
String url = springCloudConfigClientConfig.url();
if (null == url || url.isEmpty()) {
throw new IllegalArgumentException(
"The 'quarkus.spring-cloud-config.url' property cannot be empty");
}
if (url.endsWith("/")) {
return new URI(url.substring(0, url.length() - 1));
}
return new URI(url);
}

private String finalURI(String applicationName, String profile) {
private ConfigServerUrl toConfigServerUrl(String applicationName, String profile) {
URI baseURI = configServerBaseUrlProvider.get();
String path = baseURI.getPath();
List<String> finalPathSegments = new ArrayList<String>();
List<String> finalPathSegments = new ArrayList<>();
finalPathSegments.add(path);
finalPathSegments.add(applicationName);
finalPathSegments.add(profile);
if (config.label().isPresent()) {
finalPathSegments.add(config.label().get());
}
return String.join("/", finalPathSegments);
return new ConfigServerUrl(baseURI, UrlUtility.getPort(baseURI), baseURI.getHost(),
String.join("/", finalPathSegments));
}

@Override
public Uni<Response> exchange(String applicationName, String profile) {
final String requestURI = finalURI(applicationName, profile);
String finalURI = getFinalURI(applicationName, profile);
final ConfigServerUrl requestURI = toConfigServerUrl(applicationName, profile);
HttpRequest<Buffer> request = webClient
.get(getPort(baseURI), baseURI.getHost(), requestURI)
.ssl(isHttps(baseURI))
.get(requestURI.port(), requestURI.host(), requestURI.completeURLString())
.ssl(UrlUtility.isHttps(requestURI.baseURI()))
.putHeader("Accept", "application/json");
if (config.usernameAndPasswordSet()) {
request.basicAuthentication(config.username().get(), config.password().get());
}
for (Map.Entry<String, String> entry : config.headers().entrySet()) {
request.putHeader(entry.getKey(), entry.getValue());
}
log.debug("Attempting to read configuration from '" + finalURI + "'.");
log.debug("Attempting to read configuration from '" + requestURI.completeURLString() + "'.");
return request.send().map(r -> {
log.debug("Received HTTP response code '" + r.statusCode() + "'");
if (r.statusCode() != 200) {
throw new RuntimeException("Got unexpected HTTP response code " + r.statusCode()
+ " from " + finalURI);
+ " from " + requestURI.completeURLString());
} else {
String bodyAsString = r.bodyAsString();
if (bodyAsString.isEmpty()) {
throw new RuntimeException("Got empty HTTP response body " + finalURI);
throw new RuntimeException("Got empty HTTP response body " + requestURI.completeURLString());
}
try {
log.debug("Attempting to deserialize response");
Expand All @@ -215,22 +232,6 @@ public Uni<Response> exchange(String applicationName, String profile) {
});
}

private boolean isHttps(URI uri) {
return uri.getScheme().contains("https");
}

private int getPort(URI uri) {
return uri.getPort() != -1 ? uri.getPort() : (isHttps(uri) ? 443 : 80);
}

private String getFinalURI(String applicationName, String profile) {
String finalURI = baseURI.toString() + "/" + applicationName + "/" + profile;
if (config.label().isPresent()) {
finalURI += "/" + config.label().get();
}
return finalURI;
}

@Override
public void close() {
this.webClient.close();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package io.quarkus.spring.cloud.config.client.runtime.eureka;

import static java.util.stream.Collectors.toMap;

import java.util.Map;

import org.jboss.logging.Logger;

import io.quarkus.spring.cloud.config.client.runtime.SpringCloudConfigClientConfig;
import io.vertx.core.json.JsonObject;

public class DiscoveryService {

private static final Logger log = Logger.getLogger(DiscoveryService.class);
private static final String DEFAULT_ZONE = "defaultZone";

private final EurekaClient eurekaClient;

public DiscoveryService(EurekaClient eurekaClient) {
this.eurekaClient = eurekaClient;
}

public String discover(SpringCloudConfigClientConfig config) {
SpringCloudConfigClientConfig.DiscoveryConfig discoveryConfig = config.discovery().get();
validate(discoveryConfig);

String serviceId = discoveryConfig.serviceId().get();
SpringCloudConfigClientConfig.DiscoveryConfig.EurekaConfig eurekaConfig = discoveryConfig.eurekaConfig().get();
String defaultServiceUrl = eurekaConfig.serviceUrl().get(DEFAULT_ZONE);

Map<String, String> serviceUrlMap = eurekaConfig
.serviceUrl()
.entrySet()
.stream().filter(entry -> !DEFAULT_ZONE.equals(entry.getKey()))
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));

log.debug("Attempting to discover Spring Cloud Config Server URL for service '" + serviceId
+ "' using the following URLs: " + serviceUrlMap.values());
for (Map.Entry<String, String> entry : serviceUrlMap.entrySet()) {
try {
return getHomeUrl(entry.getValue(), serviceId);
} catch (Exception e) {
log.debug("Timed out while waiting for Spring Cloud Config Server URL for service '" + serviceId + "'", e);
}
}

log.debug("Fallback Attempting to discover Spring Cloud Config Server URL for service '" + serviceId
+ "' using the default URL: " + defaultServiceUrl);
try {
return getHomeUrl(defaultServiceUrl, serviceId);
} catch (Exception e) {
log.debug("Timed out while waiting for Spring Cloud Config Server URL for service '" + serviceId + "'", e);
}

throw new RuntimeException("Unable to discover Spring Cloud Config Server URL for service '" + serviceId + "'");
}

private void validate(SpringCloudConfigClientConfig.DiscoveryConfig discoveryConfig) {
if (discoveryConfig.eurekaConfig().isEmpty()) {
throw new IllegalArgumentException("No Eureka configuration has been provided");
}
if (discoveryConfig.eurekaConfig().get().serviceUrl().isEmpty()) {
throw new IllegalArgumentException("No service URLs have been configured for service");
}
if (discoveryConfig.serviceId().isEmpty()) {
throw new IllegalArgumentException("No service ID has been configured for service");
}
}

private String getHomeUrl(String defaultServiceUrl, String serviceId) {
JsonObject instance = eurekaClient.fetchInstances(defaultServiceUrl, serviceId);
return instance.getString("homePageUrl");
}

}
Loading
Loading