Skip to content
Open
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
10 changes: 10 additions & 0 deletions dependencies.toml
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ spring7 = "7.0.8"
spring-boot2 = "2.7.18"
spring-boot3 = "3.5.16"
spring-boot4 = "4.1.0"
spring-cloud-config-server = "5.0.4"
spring-cloud-context = "5.0.2"
testcontainers = "2.0.5"
thrift09 = { strictly = "0.9.3-1" }
thrift012 = { strictly = "0.12.0" }
Expand Down Expand Up @@ -1422,6 +1424,14 @@ version.ref = "spring-boot4"
module = "org.springframework.boot:spring-boot-tomcat"
version.ref = "spring-boot4"

[libraries.spring-cloud-config-server]
module = "org.springframework.cloud:spring-cloud-config-server"
version.ref = "spring-cloud-config-server"

[libraries.spring-cloud-context]
module = "org.springframework.cloud:spring-cloud-context"
version.ref = "spring-cloud-context"

[libraries.testcontainers-core]
module = "org.testcontainers:testcontainers"
[libraries.testcontainers-consul]
Expand Down
15 changes: 15 additions & 0 deletions examples/spring-cloud-config-xds/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
dependencies {
implementation project(':spring:boot4-xds')
implementation libs.spring.cloud.config.server
testImplementation libs.spring.boot4.starter.test
}

tasks.register('runConfigServer', JavaExec) {
classpath = sourceSets.main.runtimeClasspath
mainClass = 'example.springframework.boot.xds.cloudconfig.server.ConfigServerMain'
}

tasks.register('runClient', JavaExec) {
classpath = sourceSets.main.runtimeClasspath
mainClass = 'example.springframework.boot.xds.cloudconfig.client.ClientMain'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package example.springframework.boot.xds.cloudconfig.client;

import java.util.Map;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.linecorp.armeria.client.WebClient;
import com.linecorp.armeria.common.AggregatedHttpResponse;

/**
* A Spring Boot client application that loads xDS resources from a
* Spring Cloud Config Server. Properties are loaded from {@code application-client.yml}.
*
* <p>Exposes a {@code GET /relay?path=...} endpoint that forwards the request to
* the upstream resolved by the xDS listener and returns the response.
*/
@SpringBootApplication
@RestController
public class ClientMain {

private final WebClient xdsWebClient;

public ClientMain(WebClient xdsWebClient) {
this.xdsWebClient = xdsWebClient;
}

public static void main(String[] args) {
createApplication().run(args);
}

public static SpringApplication createApplication() {
final SpringApplication app = new SpringApplication(ClientMain.class);
app.setAdditionalProfiles("client");
// spring-cloud-config-server on the classpath disables the config client
// via ConfigServerBootstrapApplicationListener; re-enable it explicitly.
app.setDefaultProperties(Map.of("spring.cloud.config.enabled", "true"));
return app;
}

@GetMapping("/relay")
String relay(@RequestParam(defaultValue = "/actuator/health") String path) {
final AggregatedHttpResponse response = xdsWebClient.get(path).aggregate().join();
return response.status() + "\n" + response.contentUtf8();
}
Comment thread
jrhee17 marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package example.springframework.boot.xds.cloudconfig.client;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.linecorp.armeria.client.WebClient;
import com.linecorp.armeria.xds.XdsBootstrap;
import com.linecorp.armeria.xds.client.endpoint.XdsHttpPreprocessor;

@Configuration
class XdsClientConfig {

@Bean
XdsHttpPreprocessor xdsHttpPreprocessor(XdsBootstrap xdsBootstrap) {
return XdsHttpPreprocessor.ofListener("test-listener", xdsBootstrap);
}

@Bean
WebClient xdsWebClient(XdsHttpPreprocessor xdsHttpPreprocessor) {
return WebClient.of(xdsHttpPreprocessor);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package example.springframework.boot.xds.cloudconfig.server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

import com.linecorp.armeria.spring.xds.SpringXdsAutoConfiguration;

/**
* A Spring Cloud Config Server that serves xDS cluster definitions from a native
* (file-based) repository.
*/
@EnableConfigServer
@SpringBootApplication(exclude = SpringXdsAutoConfiguration.class)
public class ConfigServerMain {

public static void main(String[] args) {
createApplication().run(args);
}

public static SpringApplication createApplication() {
final SpringApplication app = new SpringApplication(ConfigServerMain.class);
app.setAdditionalProfiles("server", "native");
return app;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
armeria:
xds:
listener:
test-listener: |
name: test-listener

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Question) Could we use nested YAML syntax here instead?

api_listener:
api_listener:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
codec_type: AUTO
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match:
prefix: /
route:
cluster: test-cluster
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
cluster:
test-cluster: |
name: test-cluster
type: STATIC
load_assignment:
cluster_name: test-cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 8888
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
spring:
config:
"import": "configserver:"
cloud:
config:
uri: http://localhost:8888
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
server:
port: 8888

spring:
cloud:
config:
server:
native:
searchLocations: classpath:/config-repo/
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package example.springframework.boot.xds.cloudconfig;
Comment thread
jrhee17 marked this conversation as resolved.

import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;

import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.context.WebServerApplicationContext;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;

import com.linecorp.armeria.client.WebClient;
import com.linecorp.armeria.common.AggregatedHttpResponse;
import com.linecorp.armeria.common.HttpStatus;
import com.linecorp.armeria.xds.ClusterSnapshot;
import com.linecorp.armeria.xds.XdsBootstrap;

import example.springframework.boot.xds.cloudconfig.client.ClientMain;
import example.springframework.boot.xds.cloudconfig.server.ConfigServerMain;

@SpringBootTest(
classes = ClientMain.class,
properties = {
"spring.config.import=configserver:",
"spring.cloud.config.enabled=true"
})
class SpringCloudConfigXdsExampleTest {

private static ConfigurableApplicationContext server;
private static int configPort;

@BeforeAll
static void startConfigServer() {
server = ConfigServerMain.createApplication().run("--server.port=0");

configPort = ((WebServerApplicationContext) server).getWebServer().getPort();
System.setProperty("spring.cloud.config.uri", "http://localhost:" + configPort);
}

@AfterAll
static void stopConfigServer() {
System.clearProperty("spring.cloud.config.uri");
Comment thread
jrhee17 marked this conversation as resolved.
if (server != null) {
server.close();
}
}

@Autowired
WebClient xdsWebClient;

@Autowired
XdsBootstrap xdsBootstrap;

@Autowired
ConfigurableEnvironment environment;

@Autowired
ApplicationContext applicationContext;

@Test
void webClientViaXdsPreprocessor() {
// Verify the initial cluster snapshot loaded from config server (port 8888)
final AtomicReference<ClusterSnapshot> snapshotRef = new AtomicReference<>();
xdsBootstrap.clusterRoot("test-cluster")
.addSnapshotWatcher((snapshot, error) -> {
if (snapshot != null) {
snapshotRef.set(snapshot);
}
});
await().untilAsserted(() -> {
assertThat(snapshotRef.get()).isNotNull();
assertThat(endpointPort(snapshotRef.get())).isEqualTo(8888);
});

// Override the cluster endpoint to point at the config server's actual port
environment.getPropertySources()
.addFirst(new MapPropertySource("test",
Map.of("armeria.xds.cluster.test-cluster", clusterYaml(configPort))));
applicationContext.publishEvent(
new EnvironmentChangeEvent(Set.of("armeria.xds.cluster.test-cluster")));

// Wait for the xDS update to propagate
await().untilAsserted(() ->
assertThat(endpointPort(snapshotRef.get())).isEqualTo(configPort));

// Call the config server's actuator health endpoint via the xDS-resolved client
final AggregatedHttpResponse response =
xdsWebClient.get("/actuator/health").aggregate().join();
assertThat(response.status()).isEqualTo(HttpStatus.OK);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private static int endpointPort(ClusterSnapshot snapshot) {
return snapshot.xdsResource().resource()
.getLoadAssignment()
.getEndpoints(0)
.getLbEndpoints(0)
.getEndpoint()
.getAddress()
.getSocketAddress()
.getPortValue();
}

private static String clusterYaml(int port) {
return """
name: test-cluster
type: STATIC
load_assignment:
cluster_name: test-cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: %d
""".formatted(port);
}
}
2 changes: 2 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ includeWithFlags ':spring:boot4-autoconfigure', 'java17', 'publish', 'r
includeWithFlags ':spring:boot4-starter', 'java17', 'publish', 'relocate', 'no_aggregation'
includeWithFlags ':spring:boot4-webflux-autoconfigure', 'java17', 'publish', 'relocate'
includeWithFlags ':spring:boot4-webflux-starter', 'java17', 'publish', 'relocate', 'no_aggregation'
includeWithFlags ':spring:boot4-xds', 'java17', 'publish', 'relocate'
includeWithFlags ':spring:spring7', 'java17', 'publish', 'relocate'

includeWithFlags ':dropwizard2', 'java', 'publish', 'relocate'
Expand Down Expand Up @@ -332,6 +333,7 @@ includeWithFlags ':examples:spring-boot-minimal', 'java17'
includeWithFlags ':examples:spring-boot-minimal-kotlin', 'java17', 'kotlin'
includeWithFlags ':examples:spring-boot-tomcat', 'java17'
includeWithFlags ':examples:spring-boot-webflux', 'java17'
includeWithFlags ':examples:spring-cloud-config-xds', 'java17'
includeWithFlags ':examples:static-files', 'java11'
includeWithFlags ':examples:thrift', 'java11'
includeWithFlags ':examples:tutorials:grpc-tutorial', 'java11'
Expand Down
9 changes: 9 additions & 0 deletions spring/boot4-xds/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
dependencies {
api project(':xds')
api libs.spring.boot4.autoconfigure
api libs.spring.cloud.context

annotationProcessor libs.spring.boot4.configuration.processor

testImplementation libs.spring.boot4.starter.test
}
Loading
Loading