getSource() {
+ return source;
+ }
+ }
+}
diff --git a/extensions/spring-cloud-config-client/runtime/src/main/java/io/quarkus/spring/cloud/config/client/runtime/SpringCloudConfigClientConfig.java b/extensions/spring-cloud-config-client/runtime/src/main/java/io/quarkus/spring/cloud/config/client/runtime/SpringCloudConfigClientConfig.java
new file mode 100644
index 0000000000000..63abf8810fc26
--- /dev/null
+++ b/extensions/spring-cloud-config-client/runtime/src/main/java/io/quarkus/spring/cloud/config/client/runtime/SpringCloudConfigClientConfig.java
@@ -0,0 +1,64 @@
+package io.quarkus.spring.cloud.config.client.runtime;
+
+import java.time.Duration;
+import java.util.Optional;
+
+import io.quarkus.runtime.annotations.ConfigItem;
+import io.quarkus.runtime.annotations.ConfigPhase;
+import io.quarkus.runtime.annotations.ConfigRoot;
+
+@ConfigRoot(phase = ConfigPhase.BOOTSTRAP, name = SpringCloudConfigClientConfig.NAME)
+public class SpringCloudConfigClientConfig {
+
+ protected static final String NAME = "scccc";
+
+ /**
+ * If enabled, will try to read the configuration from a Spring Cloud Config Server
+ */
+ @ConfigItem(defaultValue = "false")
+ public boolean enabled;
+
+ /**
+ * If set to true, the application will not stand up if it cannot obtain configuration from the Config Server
+ */
+ @ConfigItem(defaultValue = "false")
+ public boolean failFast;
+
+ /**
+ * The Base URI where the Spring Cloud Config Server is available
+ */
+ @ConfigItem(defaultValue = "http://localhost:8888")
+ public String uri;
+
+ /**
+ * The amount of time to wait when initially establishing a connection before giving up and timing out.
+ *
+ * Specify `0` to wait indefinitely.
+ */
+ @ConfigItem(defaultValue = "10S")
+ public Duration connectionTimeout;
+
+ /**
+ * The amount of time to wait for a read on a socket before an exception is thrown.
+ *
+ * Specify `0` to wait indefinitely.
+ */
+ @ConfigItem(defaultValue = "60S")
+ public Duration readTimeout;
+
+ /**
+ * The username to be used if the Config Server has BASIC Auth enabled
+ */
+ @ConfigItem
+ public Optional username;
+
+ /**
+ * The password to be used if the Config Server has BASIC Auth enabled
+ */
+ @ConfigItem
+ public Optional password;
+
+ public boolean usernameAndPasswordSet() {
+ return username.isPresent() && password.isPresent();
+ }
+}
diff --git a/extensions/spring-cloud-config-client/runtime/src/main/java/io/quarkus/spring/cloud/config/client/runtime/SpringCloudConfigClientGateway.java b/extensions/spring-cloud-config-client/runtime/src/main/java/io/quarkus/spring/cloud/config/client/runtime/SpringCloudConfigClientGateway.java
new file mode 100644
index 0000000000000..d071f6fb42ced
--- /dev/null
+++ b/extensions/spring-cloud-config-client/runtime/src/main/java/io/quarkus/spring/cloud/config/client/runtime/SpringCloudConfigClientGateway.java
@@ -0,0 +1,8 @@
+package io.quarkus.spring.cloud.config.client.runtime;
+
+import java.io.IOException;
+
+interface SpringCloudConfigClientGateway {
+
+ Response exchange(String applicationName, String profile) throws IOException;
+}
diff --git a/extensions/spring-cloud-config-client/runtime/src/main/java/io/quarkus/spring/cloud/config/client/runtime/SpringCloudConfigClientRecorder.java b/extensions/spring-cloud-config-client/runtime/src/main/java/io/quarkus/spring/cloud/config/client/runtime/SpringCloudConfigClientRecorder.java
new file mode 100644
index 0000000000000..555b6a0035663
--- /dev/null
+++ b/extensions/spring-cloud-config-client/runtime/src/main/java/io/quarkus/spring/cloud/config/client/runtime/SpringCloudConfigClientRecorder.java
@@ -0,0 +1,48 @@
+package io.quarkus.spring.cloud.config.client.runtime;
+
+import java.util.Collections;
+
+import org.eclipse.microprofile.config.spi.ConfigSource;
+import org.eclipse.microprofile.config.spi.ConfigSourceProvider;
+import org.jboss.logging.Logger;
+
+import io.quarkus.runtime.ApplicationConfig;
+import io.quarkus.runtime.RuntimeValue;
+import io.quarkus.runtime.annotations.Recorder;
+import io.quarkus.runtime.configuration.ProfileManager;
+
+@Recorder
+public class SpringCloudConfigClientRecorder {
+
+ private static final Logger log = Logger.getLogger(SpringCloudConfigClientRecorder.class);
+
+ public RuntimeValue create(SpringCloudConfigClientConfig springCloudConfigClientConfig,
+ ApplicationConfig applicationConfig) {
+ if (!springCloudConfigClientConfig.enabled) {
+ log.debug(
+ "No attempt will be made to obtain configuration from the Spring Cloud Config Server because the functionality has been disabled via configuration");
+ return emptyRuntimeValue();
+ }
+
+ if (!applicationConfig.name.isPresent()) {
+ log.warn(
+ "No attempt will be made to obtain configuration from the Spring Cloud Config Server because the application name has not been set. Consider setting it via 'quarkus.application.name'");
+ return emptyRuntimeValue();
+ }
+
+ return new RuntimeValue<>(new SpringCloudConfigServerClientConfigSourceProvider(
+ springCloudConfigClientConfig, applicationConfig.name.get(), ProfileManager.getActiveProfile()));
+ }
+
+ private RuntimeValue emptyRuntimeValue() {
+ return new RuntimeValue<>(new EmptyConfigSourceProvider());
+ }
+
+ private static class EmptyConfigSourceProvider implements ConfigSourceProvider {
+
+ @Override
+ public Iterable getConfigSources(ClassLoader forClassLoader) {
+ return Collections.emptyList();
+ }
+ }
+}
diff --git a/extensions/spring-cloud-config-client/runtime/src/main/java/io/quarkus/spring/cloud/config/client/runtime/SpringCloudConfigServerClientConfigSourceProvider.java b/extensions/spring-cloud-config-client/runtime/src/main/java/io/quarkus/spring/cloud/config/client/runtime/SpringCloudConfigServerClientConfigSourceProvider.java
new file mode 100644
index 0000000000000..e1fa7f16a248c
--- /dev/null
+++ b/extensions/spring-cloud-config-client/runtime/src/main/java/io/quarkus/spring/cloud/config/client/runtime/SpringCloudConfigServerClientConfigSourceProvider.java
@@ -0,0 +1,97 @@
+package io.quarkus.spring.cloud.config.client.runtime;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.microprofile.config.spi.ConfigSource;
+import org.eclipse.microprofile.config.spi.ConfigSourceProvider;
+import org.jboss.logging.Logger;
+
+public class SpringCloudConfigServerClientConfigSourceProvider implements ConfigSourceProvider {
+
+ private static final Logger log = Logger.getLogger(SpringCloudConfigServerClientConfigSourceProvider.class);
+
+ private final SpringCloudConfigClientConfig springCloudConfigClientConfig;
+ private final String applicationName;
+ private final String activeProfile;
+
+ private final SpringCloudConfigClientGateway springCloudConfigClientGateway;
+
+ public SpringCloudConfigServerClientConfigSourceProvider(SpringCloudConfigClientConfig springCloudConfigClientConfig,
+ String applicationName,
+ String activeProfile) {
+ this.springCloudConfigClientConfig = springCloudConfigClientConfig;
+ this.applicationName = applicationName;
+ this.activeProfile = activeProfile;
+
+ springCloudConfigClientGateway = new DefaultSpringCloudConfigClientGateway(springCloudConfigClientConfig);
+ }
+
+ @Override
+ public Iterable getConfigSources(ClassLoader forClassLoader) {
+ try {
+ final Response response = springCloudConfigClientGateway.exchange(applicationName, activeProfile);
+ final List propertySources = response.getPropertySources();
+ Collections.reverse(propertySources); // reverse the property sources so we can increment the ordinal from lower priority to higher
+ final List result = new ArrayList<>(propertySources.size());
+ for (int i = 0; i < propertySources.size(); i++) {
+ final Response.PropertySource propertySource = propertySources.get(i);
+ // Property sources obtained from Spring Cloud Config are expected to have a higher priority than even system properties
+ // 400 is the ordinal of SysPropConfigSource, so we use 450 here
+ result.add(new InMemoryConfigSource(450 + i, propertySource.getName(), propertySource.getSource()));
+ }
+ return result;
+ } catch (Exception e) {
+ final String errorMessage = "Unable to obtain configuration from Spring Cloud Config Server at "
+ + springCloudConfigClientConfig.uri;
+ if (springCloudConfigClientConfig.failFast) {
+ throw new RuntimeException(errorMessage, e);
+ } else {
+ log.error(errorMessage, e);
+ return Collections.emptyList();
+ }
+ }
+ }
+
+ private static final class InMemoryConfigSource implements ConfigSource {
+
+ private final Map values = new HashMap<>();
+ private final int ordinal;
+ private final String name;
+
+ private InMemoryConfigSource(int ordinal, String name, Map source) {
+ this.ordinal = ordinal;
+ this.name = name;
+ this.values.putAll(source);
+ }
+
+ @Override
+ public Map getProperties() {
+ return values;
+ }
+
+ @Override
+ public Set getPropertyNames() {
+ return values.keySet();
+ }
+
+ @Override
+ public int getOrdinal() {
+ return ordinal;
+ }
+
+ @Override
+ public String getValue(String propertyName) {
+ return values.get(propertyName);
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+ }
+}
diff --git a/extensions/spring-cloud-config-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/spring-cloud-config-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml
new file mode 100644
index 0000000000000..cef00f5c55ed0
--- /dev/null
+++ b/extensions/spring-cloud-config-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml
@@ -0,0 +1,11 @@
+---
+name: "Quarkus Extension for Spring Cloud Config Client"
+metadata:
+ keywords:
+ - "spring-cloud-config-client"
+ - "spring"
+ - "config"
+ - "configuration"
+ categories:
+ - "compatibility"
+ status: "preview"
diff --git a/extensions/spring-cloud-config-client/runtime/src/test/java/io/quarkus/spring/cloud/config/client/runtime/SpringCloudConfigClientGatewayTest.java b/extensions/spring-cloud-config-client/runtime/src/test/java/io/quarkus/spring/cloud/config/client/runtime/SpringCloudConfigClientGatewayTest.java
new file mode 100644
index 0000000000000..99ffb53b7319d
--- /dev/null
+++ b/extensions/spring-cloud-config-client/runtime/src/test/java/io/quarkus/spring/cloud/config/client/runtime/SpringCloudConfigClientGatewayTest.java
@@ -0,0 +1,81 @@
+package io.quarkus.spring.cloud.config.client.runtime;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.entry;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.time.Duration;
+import java.util.Optional;
+
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.client.WireMock;
+
+class SpringCloudConfigClientGatewayTest {
+
+ private static final int MOCK_SERVER_PORT = 8089;
+ private static final WireMockServer wireMockServer = new WireMockServer(MOCK_SERVER_PORT);
+
+ private final SpringCloudConfigClientGateway sut = new DefaultSpringCloudConfigClientGateway(
+ configForTesting());
+
+ @BeforeAll
+ static void start() {
+ wireMockServer.start();
+ }
+
+ @AfterAll
+ static void stop() {
+ wireMockServer.stop();
+ }
+
+ @Test
+ void testBasicExchange() throws IOException {
+ final String applicationName = "foo";
+ final String profile = "dev";
+ wireMockServer.stubFor(WireMock.get(String.format("/%s/%s", applicationName, profile)).willReturn(WireMock
+ .okJson(getJsonStringForApplicationAndProfile(applicationName, profile))));
+
+ final Response response = sut.exchange(applicationName, profile);
+
+ assertThat(response).isNotNull().satisfies(r -> {
+ assertThat(r.getName()).isEqualTo("foo");
+ assertThat(r.getProfiles()).containsExactly("dev");
+ assertThat(r.getPropertySources()).hasSize(4);
+ assertThat(r.getPropertySources().get(0)).satisfies(ps -> {
+ assertThat(ps.getSource()).contains(entry("bar", "spam"), entry("foo", "from foo development"),
+ entry("democonfigclient.message", "hello from dev profile"));
+ });
+ assertThat(r.getPropertySources().get(1)).satisfies(ps -> {
+ assertThat(ps.getSource()).contains(entry("my.prop", "from application-dev.yml"));
+ });
+ assertThat(r.getPropertySources().get(2)).satisfies(ps -> {
+ assertThat(ps.getSource()).contains(entry("foo", "from foo props"),
+ entry("democonfigclient.message", "hello spring io"));
+ });
+ assertThat(r.getPropertySources().get(3)).satisfies(ps -> {
+ assertThat(ps.getSource()).contains(entry("foo", "baz"));
+ });
+ });
+ }
+
+ private String getJsonStringForApplicationAndProfile(String applicationName, String profile) throws IOException {
+ return IOUtils.toString(this.getClass().getResourceAsStream(String.format("/%s-%s.json", applicationName, profile)),
+ Charset.defaultCharset());
+ }
+
+ private static SpringCloudConfigClientConfig configForTesting() {
+ SpringCloudConfigClientConfig springCloudConfigClientConfig = new SpringCloudConfigClientConfig();
+ springCloudConfigClientConfig.uri = "http://localhost:" + MOCK_SERVER_PORT;
+ springCloudConfigClientConfig.connectionTimeout = Duration.ZERO;
+ springCloudConfigClientConfig.readTimeout = Duration.ZERO;
+ springCloudConfigClientConfig.username = Optional.empty();
+ springCloudConfigClientConfig.password = Optional.empty();
+ return springCloudConfigClientConfig;
+ }
+}
diff --git a/extensions/spring-cloud-config-client/runtime/src/test/resources/foo-dev.json b/extensions/spring-cloud-config-client/runtime/src/test/resources/foo-dev.json
new file mode 100644
index 0000000000000..85fcf86bddc50
--- /dev/null
+++ b/extensions/spring-cloud-config-client/runtime/src/test/resources/foo-dev.json
@@ -0,0 +1,42 @@
+{
+
+ "name": "foo",
+ "profiles": [
+ "dev"
+ ],
+ "label": "master",
+ "version": "bb51f4173258ae3481c61b95b503c13862ccfba7",
+ "state": null,
+ "propertySources": [
+ {
+ "name": "https://github.com/spring-cloud-samples/config-repo/foo-dev.yml",
+ "source": {
+ "bar": "spam",
+ "foo": "from foo development",
+ "democonfigclient.message": "hello from dev profile"
+ }
+ },
+ {
+ "name": "https://github.com/spring-cloud-samples/config-repo/application-dev.yml",
+ "source": {
+ "my.prop": "from application-dev.yml"
+ }
+ },
+ {
+ "name": "https://github.com/spring-cloud-samples/config-repo/foo.properties",
+ "source": {
+ "democonfigclient.message": "hello spring io",
+ "foo": "from foo props"
+ }
+ },
+ {
+ "name": "https://github.com/spring-cloud-samples/config-repo/application.yml",
+ "source": {
+ "info.description": "Spring Cloud Samples",
+ "info.url": "https://github.com/spring-cloud-samples",
+ "eureka.client.serviceUrl.defaultZone": "http://localhost:8761/eureka/",
+ "foo": "baz"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml
index caee2b865dd63..49f786ab969a5 100644
--- a/integration-tests/pom.xml
+++ b/integration-tests/pom.xml
@@ -48,6 +48,7 @@
spring-web
spring-data-jpa
spring-boot-properties
+ spring-cloud-config-client
infinispan-cache-jpa
elytron-security
elytron-security-oauth2
diff --git a/integration-tests/spring-cloud-config-client/pom.xml b/integration-tests/spring-cloud-config-client/pom.xml
new file mode 100644
index 0000000000000..4c2d649130fa8
--- /dev/null
+++ b/integration-tests/spring-cloud-config-client/pom.xml
@@ -0,0 +1,123 @@
+
+
+
+ quarkus-integration-tests-parent
+ io.quarkus
+ 999-SNAPSHOT
+
+ 4.0.0
+
+ quarkus-integration-test-spring-cloud-config-client
+ Quarkus - Integration Tests - Spring Cloud Config Client
+
+
+
+ io.quarkus
+ quarkus-resteasy
+
+
+ io.quarkus
+ quarkus-spring-cloud-config-client
+
+
+ io.quarkus
+ quarkus-junit5
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+
+
+
+
+ io.quarkus
+ quarkus-maven-plugin
+ ${project.version}
+
+
+
+ build
+
+
+
+
+
+
+
+
+
+ native
+
+
+ native
+
+
+
+
+
+ maven-failsafe-plugin
+
+
+
+ integration-test
+ verify
+
+
+
+ ${project.build.directory}/${project.build.finalName}-runner
+
+
+
+
+
+
+ uk.co.automatictester
+ wiremock-maven-plugin
+
+
+ wiremock-start
+ compile
+
+ stop
+ run
+
+
+ target/classes
+ --port=8089 --disable-banner
+
+
+
+ wiremock-stop
+ post-integration-test
+
+ stop
+
+
+
+
+
+ com.github.tomakehurst
+ wiremock
+ 2.25.1
+
+
+
+
+
+
+ native
+
+
+
+
\ No newline at end of file
diff --git a/integration-tests/spring-cloud-config-client/src/main/java/io/quarkus/it/spring/config/server/client/GreetingResource.java b/integration-tests/spring-cloud-config-client/src/main/java/io/quarkus/it/spring/config/server/client/GreetingResource.java
new file mode 100644
index 0000000000000..5c8603cdf1b60
--- /dev/null
+++ b/integration-tests/spring-cloud-config-client/src/main/java/io/quarkus/it/spring/config/server/client/GreetingResource.java
@@ -0,0 +1,18 @@
+package io.quarkus.it.spring.config.server.client;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+@Path("/greeting")
+public class GreetingResource {
+
+ @ConfigProperty(name = "greeting.message")
+ String message;
+
+ @GET
+ public String greet() {
+ return message;
+ }
+}
diff --git a/integration-tests/spring-cloud-config-client/src/main/resources/__files/a-bootiful-client-prod.json b/integration-tests/spring-cloud-config-client/src/main/resources/__files/a-bootiful-client-prod.json
new file mode 100644
index 0000000000000..ab01d933fac00
--- /dev/null
+++ b/integration-tests/spring-cloud-config-client/src/main/resources/__files/a-bootiful-client-prod.json
@@ -0,0 +1,26 @@
+{
+ "name": "a-bootiful-client",
+ "profiles": [
+ "prod"
+ ],
+ "label": "master",
+ "version": "bb51f4173258ae3481c61b95b503c13862ccfba7",
+ "state": null,
+ "propertySources": [
+ {
+ "name": "https://github.com/spring-cloud-samples/config-repo/testapp-prod.yml",
+ "source": {
+ "bar": "spam",
+ "foo": "from foo development",
+ "greeting.message": "hello from spring cloud config server"
+ }
+ },
+ {
+ "name": "https://github.com/spring-cloud-samples/config-repo/application.yml",
+ "source": {
+ "info.description": "Sample",
+ "foo": "baz"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/integration-tests/spring-cloud-config-client/src/main/resources/application.properties b/integration-tests/spring-cloud-config-client/src/main/resources/application.properties
new file mode 100644
index 0000000000000..7e46df5c06ca3
--- /dev/null
+++ b/integration-tests/spring-cloud-config-client/src/main/resources/application.properties
@@ -0,0 +1,7 @@
+quarkus.application.name=a-bootiful-client
+%prod.quarkus.scccc.uri=http://localhost:8089
+%prod.quarkus.scccc.username=user
+%prod.quarkus.scccc.password=pass
+%prod.quarkus.scccc.enabled=true
+
+greeting.message=hello from application.properties
\ No newline at end of file
diff --git a/integration-tests/spring-cloud-config-client/src/main/resources/mappings/a-bootiful-client-prod.json b/integration-tests/spring-cloud-config-client/src/main/resources/mappings/a-bootiful-client-prod.json
new file mode 100644
index 0000000000000..c297faba491d6
--- /dev/null
+++ b/integration-tests/spring-cloud-config-client/src/main/resources/mappings/a-bootiful-client-prod.json
@@ -0,0 +1,17 @@
+{
+ "request": {
+ "method": "GET",
+ "urlPattern": "/a-bootiful-client/prod",
+ "basicAuth" : {
+ "username" : "user",
+ "password" : "pass"
+ }
+ },
+ "response": {
+ "status": 200,
+ "headers": {
+ "Content-Type": "application/json"
+ },
+ "bodyFileName": "a-bootiful-client-prod.json"
+ }
+}
\ No newline at end of file
diff --git a/integration-tests/spring-cloud-config-client/src/test/java/io/quarkus/spring/cloud/config/client/runtime/GreetingResourceIT.java b/integration-tests/spring-cloud-config-client/src/test/java/io/quarkus/spring/cloud/config/client/runtime/GreetingResourceIT.java
new file mode 100644
index 0000000000000..7b169a30b08e5
--- /dev/null
+++ b/integration-tests/spring-cloud-config-client/src/test/java/io/quarkus/spring/cloud/config/client/runtime/GreetingResourceIT.java
@@ -0,0 +1,12 @@
+package io.quarkus.spring.cloud.config.client.runtime;
+
+import io.quarkus.test.junit.NativeImageTest;
+
+@NativeImageTest
+public class GreetingResourceIT extends GreetingResourceTest {
+
+ @Override
+ protected String getExpectedValue() {
+ return "hello from spring cloud config server";
+ }
+}
diff --git a/integration-tests/spring-cloud-config-client/src/test/java/io/quarkus/spring/cloud/config/client/runtime/GreetingResourceTest.java b/integration-tests/spring-cloud-config-client/src/test/java/io/quarkus/spring/cloud/config/client/runtime/GreetingResourceTest.java
new file mode 100644
index 0000000000000..50aabe5bac9d6
--- /dev/null
+++ b/integration-tests/spring-cloud-config-client/src/test/java/io/quarkus/spring/cloud/config/client/runtime/GreetingResourceTest.java
@@ -0,0 +1,26 @@
+package io.quarkus.spring.cloud.config.client.runtime;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.CoreMatchers.is;
+
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit.QuarkusTest;
+
+@QuarkusTest
+class GreetingResourceTest {
+
+ @Test
+ void testGreeting() {
+ given()
+ .when().get("/greeting")
+ .then()
+ .statusCode(200)
+ .body(is(getExpectedValue()));
+
+ }
+
+ protected String getExpectedValue() {
+ return "hello from application.properties";
+ }
+}