Skip to content

Commit 26487ea

Browse files
committed
minimal impl
1 parent 5567e0d commit 26487ea

41 files changed

Lines changed: 3275 additions & 229 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2025 LINE Corporation
3+
*
4+
* LINE Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. 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, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
package com.linecorp.armeria.server;
18+
19+
import com.linecorp.armeria.common.annotation.Nullable;
20+
import com.linecorp.armeria.common.annotation.UnstableApi;
21+
22+
@UnstableApi
23+
interface ConnectionLevelSetters {
24+
25+
@Nullable
26+
ConnectionAcceptor connectionAcceptor();
27+
28+
ConnectionLevelSetters connectionAcceptor(ConnectionAcceptor connectionAcceptor);
29+
30+
@Nullable
31+
ServerTlsProvider tlsProvider();
32+
33+
ConnectionLevelSetters tlsProvider(ServerTlsProvider serverTlsProvider);
34+
}

core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,8 @@
172172
*
173173
* @see VirtualHostBuilder
174174
*/
175-
public final class ServerBuilder implements TlsSetters, ServiceConfigsBuilder<ServerBuilder> {
175+
public final class ServerBuilder implements ConnectionLevelSetters, TlsSetters,
176+
ServiceConfigsBuilder<ServerBuilder> {
176177
private static final Logger logger = LoggerFactory.getLogger(ServerBuilder.class);
177178

178179
// Defaults to no graceful shutdown.
@@ -531,12 +532,22 @@ public ServerBuilder childChannelPipelineCustomizer(
531532
* Sets the {@link ConnectionAcceptor} that is called once per connection, before TLS
532533
* negotiation, to decide whether to accept the connection.
533534
*/
535+
@Override
534536
@UnstableApi
535537
public ServerBuilder connectionAcceptor(ConnectionAcceptor connectionAcceptor) {
536538
this.connectionAcceptor = requireNonNull(connectionAcceptor, "connectionAcceptor");
537539
return this;
538540
}
539541

542+
/**
543+
* Returns the {@link ConnectionAcceptor} configured so far.
544+
*/
545+
@Override
546+
@UnstableApi
547+
public ConnectionAcceptor connectionAcceptor() {
548+
return connectionAcceptor;
549+
}
550+
540551
/**
541552
* Adds a {@link ServerPlugin} that will be installed during {@link Server} construction
542553
* and during {@link Server#reconfigure(ServerConfigurator)}.
@@ -1243,6 +1254,16 @@ public ServerBuilder tls(KeyManagerFactory keyManagerFactory) {
12431254
return this;
12441255
}
12451256

1257+
/**
1258+
* Returns the {@link ServerTlsProvider} configured so far, or {@code null} if not set.
1259+
*/
1260+
@Override
1261+
@UnstableApi
1262+
@Nullable
1263+
public ServerTlsProvider tlsProvider() {
1264+
return serverTlsProviderBuilder.serverTlsProvider;
1265+
}
1266+
12461267
/**
12471268
* Sets the specified {@link TlsProvider} which will be used for building an {@link SslContext} of
12481269
* a hostname.
@@ -1322,6 +1343,7 @@ public ServerBuilder tlsProvider(TlsProvider tlsProvider, ServerTlsConfig tlsCon
13221343
* the {@code tlsProvider} takes priority. The static TLS settings are used as a fallback
13231344
* when the provider returns {@code null}.
13241345
*/
1346+
@Override
13251347
@UnstableApi
13261348
public ServerBuilder tlsProvider(ServerTlsProvider serverTlsProvider) {
13271349
requireNonNull(serverTlsProvider, "serverTlsProvider");

it/xds-client/src/test/java/com/linecorp/armeria/xds/it/XdsCertificateExtension.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,14 @@
5252
* new XdsCertificateExtension(new SelfSignedCertificateExtension("localhost"));
5353
* }</pre>
5454
*/
55-
final class XdsCertificateExtension extends AbstractAllOrEachExtension {
55+
public final class XdsCertificateExtension extends AbstractAllOrEachExtension {
5656

5757
private final SelfSignedCertificateExtension delegate;
5858
private Path tempDir;
5959
private File certificateFile;
6060
private File privateKeyFile;
6161

62-
XdsCertificateExtension(SelfSignedCertificateExtension delegate) {
62+
public XdsCertificateExtension(SelfSignedCertificateExtension delegate) {
6363
this.delegate = delegate;
6464
}
6565

@@ -79,23 +79,23 @@ protected void after(ExtensionContext context) throws Exception {
7979
}
8080
}
8181

82-
File certificateFile() {
82+
public File certificateFile() {
8383
return certificateFile;
8484
}
8585

86-
File privateKeyFile() {
86+
public File privateKeyFile() {
8787
return privateKeyFile;
8888
}
8989

90-
X509Certificate certificate() {
90+
public X509Certificate certificate() {
9191
return delegate.certificate();
9292
}
9393

94-
PrivateKey privateKey() {
94+
public PrivateKey privateKey() {
9595
return delegate.privateKey();
9696
}
9797

98-
TlsKeyPair tlsKeyPair() {
98+
public TlsKeyPair tlsKeyPair() {
9999
return delegate.tlsKeyPair();
100100
}
101101

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
/*
2+
* Copyright 2026 LY Corporation
3+
*
4+
* LY Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. 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, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
package com.linecorp.armeria.xds.it;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.awaitility.Awaitility.await;
20+
21+
import java.util.List;
22+
import java.util.concurrent.atomic.AtomicLong;
23+
import java.util.concurrent.atomic.AtomicReference;
24+
import java.util.function.Consumer;
25+
26+
import org.junit.jupiter.api.extension.ExtensionContext;
27+
28+
import com.google.common.collect.ImmutableList;
29+
30+
import com.linecorp.armeria.common.SessionProtocol;
31+
import com.linecorp.armeria.server.Server;
32+
import com.linecorp.armeria.server.grpc.GrpcService;
33+
import com.linecorp.armeria.testing.junit5.common.AbstractAllOrEachExtension;
34+
import com.linecorp.armeria.xds.ListenerRoot;
35+
import com.linecorp.armeria.xds.ListenerSnapshot;
36+
import com.linecorp.armeria.xds.XdsBootstrap;
37+
import com.linecorp.armeria.xds.XdsBootstrapBuilder;
38+
39+
import io.envoyproxy.controlplane.cache.v3.SimpleCache;
40+
import io.envoyproxy.controlplane.cache.v3.Snapshot;
41+
import io.envoyproxy.controlplane.server.V3DiscoveryServer;
42+
import io.envoyproxy.envoy.config.bootstrap.v3.Bootstrap;
43+
import io.envoyproxy.envoy.config.cluster.v3.Cluster;
44+
import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment;
45+
import io.envoyproxy.envoy.config.listener.v3.Listener;
46+
import io.envoyproxy.envoy.config.route.v3.RouteConfiguration;
47+
import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.Secret;
48+
49+
/**
50+
* A JUnit 5 extension that manages an xDS control plane server, an internal
51+
* {@link XdsBootstrap}, and a mutable resource map. All standard discovery services
52+
* (ADS, LDS, RDS, CDS, EDS, SDS) are registered on a single ephemeral HTTP port.
53+
*
54+
* <p>Usage:
55+
* <pre>{@code
56+
* @RegisterExtension
57+
* @Order(0)
58+
* static final XdsControlPlaneExtension controlPlane = new XdsControlPlaneExtension();
59+
*
60+
* @RegisterExtension
61+
* @Order(1)
62+
* static final ServerExtension server = new ServerExtension() {
63+
* protected void configure(ServerBuilder sb) {
64+
* controlPlane.set(myListener);
65+
* sb.plugin(XdsServerPlugin.of(controlPlane.bootstrap(), "my-listener"));
66+
* sb.service("/hello", (ctx, req) -> HttpResponse.of("hello"));
67+
* }
68+
* };
69+
*
70+
* @Test
71+
* void test() {
72+
* String ver = controlPlane.set(updatedListener);
73+
* controlPlane.awaitListener("my-listener", ver);
74+
* // make assertions...
75+
* }
76+
* }</pre>
77+
*/
78+
public final class XdsControlPlaneExtension extends AbstractAllOrEachExtension {
79+
80+
private static final String GROUP = "key";
81+
82+
private final SimpleCache<String> cache = new SimpleCache<>(node -> GROUP);
83+
private final AtomicLong version = new AtomicLong();
84+
private final Consumer<XdsBootstrapBuilder> bootstrapCustomizer;
85+
86+
private List<Listener> listeners = ImmutableList.of();
87+
private List<Cluster> clusters = ImmutableList.of();
88+
private List<ClusterLoadAssignment> endpoints = ImmutableList.of();
89+
private List<RouteConfiguration> routes = ImmutableList.of();
90+
private List<Secret> secrets = ImmutableList.of();
91+
92+
private Server server;
93+
private XdsBootstrap xdsBootstrap;
94+
95+
public XdsControlPlaneExtension() {
96+
this(builder -> {});
97+
}
98+
99+
public XdsControlPlaneExtension(Consumer<XdsBootstrapBuilder> bootstrapCustomizer) {
100+
this.bootstrapCustomizer = bootstrapCustomizer;
101+
}
102+
103+
@Override
104+
protected void before(ExtensionContext context) throws Exception {
105+
final V3DiscoveryServer ds = new V3DiscoveryServer(cache);
106+
server = Server.builder()
107+
.service(GrpcService.builder()
108+
.addService(ds.getAggregatedDiscoveryServiceImpl())
109+
.addService(ds.getListenerDiscoveryServiceImpl())
110+
.addService(ds.getRouteDiscoveryServiceImpl())
111+
.addService(ds.getClusterDiscoveryServiceImpl())
112+
.addService(ds.getEndpointDiscoveryServiceImpl())
113+
.addService(ds.getSecretDiscoveryServiceImpl())
114+
.build())
115+
.http(0)
116+
.build();
117+
server.start().join();
118+
119+
final int port = server.activePort(SessionProtocol.HTTP).localAddress().getPort();
120+
//language=YAML
121+
final String yaml =
122+
"""
123+
dynamic_resources:
124+
lds_config:
125+
api_config_source:
126+
api_type: GRPC
127+
grpc_services:
128+
- envoy_grpc:
129+
cluster_name: xds-cluster
130+
cds_config:
131+
api_config_source:
132+
api_type: GRPC
133+
grpc_services:
134+
- envoy_grpc:
135+
cluster_name: xds-cluster
136+
static_resources:
137+
clusters:
138+
- name: xds-cluster
139+
type: STATIC
140+
load_assignment:
141+
cluster_name: xds-cluster
142+
endpoints:
143+
- lb_endpoints:
144+
- endpoint:
145+
address:
146+
socket_address:
147+
address: 127.0.0.1
148+
port_value: %d
149+
""".formatted(port);
150+
final XdsBootstrapBuilder builder =
151+
XdsBootstrap.builder(XdsResourceReader.fromYaml(yaml, Bootstrap.class));
152+
bootstrapCustomizer.accept(builder);
153+
xdsBootstrap = builder.build();
154+
}
155+
156+
@Override
157+
protected void after(ExtensionContext context) throws Exception {
158+
if (xdsBootstrap != null) {
159+
xdsBootstrap.close();
160+
}
161+
if (server != null) {
162+
server.stop().join();
163+
}
164+
}
165+
166+
public XdsBootstrap bootstrap() {
167+
return xdsBootstrap;
168+
}
169+
170+
public SimpleCache<String> cache() {
171+
return cache;
172+
}
173+
174+
public int httpPort() {
175+
return server.activePort(SessionProtocol.HTTP).localAddress().getPort();
176+
}
177+
178+
public String set(Listener... listeners) {
179+
this.listeners = ImmutableList.copyOf(listeners);
180+
return pushSnapshot();
181+
}
182+
183+
public String set(Cluster... clusters) {
184+
this.clusters = ImmutableList.copyOf(clusters);
185+
return pushSnapshot();
186+
}
187+
188+
public String set(ClusterLoadAssignment... endpoints) {
189+
this.endpoints = ImmutableList.copyOf(endpoints);
190+
return pushSnapshot();
191+
}
192+
193+
public String set(RouteConfiguration... routes) {
194+
this.routes = ImmutableList.copyOf(routes);
195+
return pushSnapshot();
196+
}
197+
198+
public String set(Secret... secrets) {
199+
this.secrets = ImmutableList.copyOf(secrets);
200+
return pushSnapshot();
201+
}
202+
203+
/**
204+
* Waits until the internal {@link XdsBootstrap} has received the specified version
205+
* for the given listener name.
206+
*/
207+
public void awaitListener(String listenerName, String version) {
208+
try (ListenerRoot root = xdsBootstrap.listenerRoot(listenerName)) {
209+
final AtomicReference<ListenerSnapshot> ref = new AtomicReference<>();
210+
root.addSnapshotWatcher((snapshot, error) -> {
211+
if (snapshot != null) {
212+
ref.set(snapshot);
213+
}
214+
});
215+
await().untilAsserted(() -> {
216+
final ListenerSnapshot snapshot = ref.get();
217+
assertThat(snapshot).isNotNull();
218+
assertThat(snapshot.xdsResource().version()).isEqualTo(version);
219+
});
220+
}
221+
}
222+
223+
private String pushSnapshot() {
224+
final String ver = String.valueOf(version.incrementAndGet());
225+
cache.setSnapshot(GROUP, Snapshot.create(clusters, endpoints, listeners,
226+
routes, secrets, ver));
227+
return ver;
228+
}
229+
}

0 commit comments

Comments
 (0)