Skip to content

Commit c1d0e14

Browse files
authored
xds: Fake control plane test setup code to Rules (#9666)
This extracts the startup and shutdown code for the control and data plane server to reparate JUnit rules, which allows this logic to be resued in other tests in a simple manner. Also makes the test easier to read with the boiler plate init code removed.
1 parent 39c2646 commit c1d0e14

File tree

3 files changed

+529
-365
lines changed

3 files changed

+529
-365
lines changed
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
/*
2+
* Copyright 2022 The gRPC Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://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,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc.xds;
18+
19+
import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_CDS;
20+
import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_EDS;
21+
import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_LDS;
22+
import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_RDS;
23+
24+
import com.google.common.collect.ImmutableMap;
25+
import com.google.protobuf.Any;
26+
import com.google.protobuf.Message;
27+
import com.google.protobuf.UInt32Value;
28+
import io.envoyproxy.envoy.config.cluster.v3.Cluster;
29+
import io.envoyproxy.envoy.config.core.v3.Address;
30+
import io.envoyproxy.envoy.config.core.v3.AggregatedConfigSource;
31+
import io.envoyproxy.envoy.config.core.v3.ConfigSource;
32+
import io.envoyproxy.envoy.config.core.v3.HealthStatus;
33+
import io.envoyproxy.envoy.config.core.v3.SocketAddress;
34+
import io.envoyproxy.envoy.config.core.v3.TrafficDirection;
35+
import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment;
36+
import io.envoyproxy.envoy.config.endpoint.v3.Endpoint;
37+
import io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint;
38+
import io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints;
39+
import io.envoyproxy.envoy.config.listener.v3.ApiListener;
40+
import io.envoyproxy.envoy.config.listener.v3.Filter;
41+
import io.envoyproxy.envoy.config.listener.v3.FilterChain;
42+
import io.envoyproxy.envoy.config.listener.v3.FilterChainMatch;
43+
import io.envoyproxy.envoy.config.listener.v3.Listener;
44+
import io.envoyproxy.envoy.config.route.v3.NonForwardingAction;
45+
import io.envoyproxy.envoy.config.route.v3.Route;
46+
import io.envoyproxy.envoy.config.route.v3.RouteAction;
47+
import io.envoyproxy.envoy.config.route.v3.RouteConfiguration;
48+
import io.envoyproxy.envoy.config.route.v3.RouteMatch;
49+
import io.envoyproxy.envoy.config.route.v3.VirtualHost;
50+
import io.envoyproxy.envoy.extensions.filters.http.router.v3.Router;
51+
import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager;
52+
import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter;
53+
import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.Rds;
54+
import io.grpc.NameResolverRegistry;
55+
import io.grpc.Server;
56+
import io.grpc.netty.NettyServerBuilder;
57+
import java.util.Collections;
58+
import java.util.Map;
59+
import java.util.UUID;
60+
import java.util.concurrent.TimeUnit;
61+
import java.util.logging.Level;
62+
import java.util.logging.Logger;
63+
import org.junit.rules.TestWatcher;
64+
import org.junit.runner.Description;
65+
66+
/**
67+
* Starts a control plane server and sets up the test to use it. Initialized with a default
68+
* configuration, but also provides methods for updating the configuration.
69+
*/
70+
public class ControlPlaneRule extends TestWatcher {
71+
private static final Logger logger = Logger.getLogger(ControlPlaneRule.class.getName());
72+
73+
private static final String SCHEME = "test-xds";
74+
private static final String RDS_NAME = "route-config.googleapis.com";
75+
private static final String CLUSTER_NAME = "cluster0";
76+
private static final String EDS_NAME = "eds-service-0";
77+
private static final String SERVER_LISTENER_TEMPLATE_NO_REPLACEMENT =
78+
"grpc/server?udpa.resource.listening_address=";
79+
private static final String SERVER_HOST_NAME = "test-server";
80+
private static final String HTTP_CONNECTION_MANAGER_TYPE_URL =
81+
"type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3"
82+
+ ".HttpConnectionManager";
83+
84+
private Server server;
85+
private XdsTestControlPlaneService controlPlaneService;
86+
private XdsNameResolverProvider nameResolverProvider;
87+
88+
/**
89+
* Returns the test control plane service interface.
90+
*/
91+
public XdsTestControlPlaneService getService() {
92+
return controlPlaneService;
93+
}
94+
95+
/**
96+
* Returns the server instance.
97+
*/
98+
public Server getServer() {
99+
return server;
100+
}
101+
102+
@Override protected void starting(Description description) {
103+
// Start the control plane server.
104+
try {
105+
controlPlaneService = new XdsTestControlPlaneService();
106+
NettyServerBuilder controlPlaneServerBuilder = NettyServerBuilder.forPort(0)
107+
.addService(controlPlaneService);
108+
server = controlPlaneServerBuilder.build().start();
109+
} catch (Exception e) {
110+
throw new AssertionError("unable to start the control plane server", e);
111+
}
112+
113+
// Configure and register an xDS name resolver so that gRPC knows how to connect to the server.
114+
nameResolverProvider = XdsNameResolverProvider.createForTest(SCHEME,
115+
defaultBootstrapOverride());
116+
NameResolverRegistry.getDefaultRegistry().register(nameResolverProvider);
117+
}
118+
119+
@Override protected void finished(Description description) {
120+
if (server != null) {
121+
server.shutdownNow();
122+
try {
123+
if (!server.awaitTermination(5, TimeUnit.SECONDS)) {
124+
logger.log(Level.SEVERE, "Timed out waiting for server shutdown");
125+
}
126+
} catch (InterruptedException e) {
127+
throw new AssertionError("unable to shut down control plane server", e);
128+
}
129+
}
130+
NameResolverRegistry.getDefaultRegistry().deregister(nameResolverProvider);
131+
}
132+
133+
/**
134+
* For test purpose, use boostrapOverride to programmatically provide bootstrap info.
135+
*/
136+
public Map<String, ?> defaultBootstrapOverride() {
137+
return ImmutableMap.of(
138+
"node", ImmutableMap.of(
139+
"id", UUID.randomUUID().toString(),
140+
"cluster", "cluster0"),
141+
"xds_servers", Collections.singletonList(
142+
143+
ImmutableMap.of(
144+
"server_uri", "localhost:" + server.getPort(),
145+
"channel_creds", Collections.singletonList(
146+
ImmutableMap.of("type", "insecure")
147+
),
148+
"server_features", Collections.singletonList("xds_v3")
149+
)
150+
),
151+
"server_listener_resource_name_template", SERVER_LISTENER_TEMPLATE_NO_REPLACEMENT
152+
);
153+
}
154+
155+
void setLdsConfig(Listener serverListener, Listener clientListener) {
156+
getService().setXdsConfig(ADS_TYPE_URL_LDS,
157+
ImmutableMap.of(SERVER_LISTENER_TEMPLATE_NO_REPLACEMENT, serverListener,
158+
SERVER_HOST_NAME, clientListener));
159+
}
160+
161+
void setRdsConfig(RouteConfiguration routeConfiguration) {
162+
getService().setXdsConfig(ADS_TYPE_URL_RDS, ImmutableMap.of(RDS_NAME, routeConfiguration));
163+
}
164+
165+
void setCdsConfig(Cluster cluster) {
166+
getService().setXdsConfig(ADS_TYPE_URL_CDS,
167+
ImmutableMap.<String, Message>of(CLUSTER_NAME, cluster));
168+
}
169+
170+
void setEdsConfig(ClusterLoadAssignment clusterLoadAssignment) {
171+
getService().setXdsConfig(ADS_TYPE_URL_EDS,
172+
ImmutableMap.<String, Message>of(EDS_NAME, clusterLoadAssignment));
173+
}
174+
175+
/**
176+
* Builds a new default RDS configuration.
177+
*/
178+
static RouteConfiguration buildRouteConfiguration(String authority) {
179+
io.envoyproxy.envoy.config.route.v3.VirtualHost virtualHost = VirtualHost.newBuilder()
180+
.addDomains(authority)
181+
.addRoutes(
182+
Route.newBuilder()
183+
.setMatch(
184+
RouteMatch.newBuilder().setPrefix("/").build())
185+
.setRoute(
186+
RouteAction.newBuilder().setCluster(CLUSTER_NAME).build()).build()).build();
187+
return RouteConfiguration.newBuilder().setName(RDS_NAME).addVirtualHosts(virtualHost).build();
188+
}
189+
190+
/**
191+
* Builds a new default CDS configuration.
192+
*/
193+
static Cluster buildCluster() {
194+
return Cluster.newBuilder()
195+
.setName(CLUSTER_NAME)
196+
.setType(Cluster.DiscoveryType.EDS)
197+
.setEdsClusterConfig(
198+
Cluster.EdsClusterConfig.newBuilder()
199+
.setServiceName(EDS_NAME)
200+
.setEdsConfig(
201+
ConfigSource.newBuilder()
202+
.setAds(AggregatedConfigSource.newBuilder().build())
203+
.build())
204+
.build())
205+
.setLbPolicy(Cluster.LbPolicy.ROUND_ROBIN)
206+
.build();
207+
}
208+
209+
/**
210+
* Builds a new default EDS configuration.
211+
*/
212+
static ClusterLoadAssignment buildClusterLoadAssignment(String hostName, int port) {
213+
Address address = Address.newBuilder()
214+
.setSocketAddress(
215+
SocketAddress.newBuilder().setAddress(hostName).setPortValue(port).build()).build();
216+
LocalityLbEndpoints endpoints = LocalityLbEndpoints.newBuilder()
217+
.setLoadBalancingWeight(UInt32Value.of(10))
218+
.setPriority(0)
219+
.addLbEndpoints(
220+
LbEndpoint.newBuilder()
221+
.setEndpoint(
222+
Endpoint.newBuilder().setAddress(address).build())
223+
.setHealthStatus(HealthStatus.HEALTHY)
224+
.build()).build();
225+
return ClusterLoadAssignment.newBuilder()
226+
.setClusterName(EDS_NAME)
227+
.addEndpoints(endpoints)
228+
.build();
229+
}
230+
231+
/**
232+
* Builds a new client listener.
233+
*/
234+
static Listener buildClientListener(String name) {
235+
HttpFilter httpFilter = HttpFilter.newBuilder()
236+
.setName("terminal-filter")
237+
.setTypedConfig(Any.pack(Router.newBuilder().build()))
238+
.setIsOptional(true)
239+
.build();
240+
ApiListener apiListener = ApiListener.newBuilder().setApiListener(Any.pack(
241+
io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3
242+
.HttpConnectionManager.newBuilder()
243+
.setRds(
244+
Rds.newBuilder()
245+
.setRouteConfigName(RDS_NAME)
246+
.setConfigSource(
247+
ConfigSource.newBuilder()
248+
.setAds(AggregatedConfigSource.getDefaultInstance())))
249+
.addAllHttpFilters(Collections.singletonList(httpFilter))
250+
.build(),
251+
HTTP_CONNECTION_MANAGER_TYPE_URL)).build();
252+
return Listener.newBuilder()
253+
.setName(name)
254+
.setApiListener(apiListener).build();
255+
}
256+
257+
/**
258+
* Builds a new server listener.
259+
*/
260+
static Listener buildServerListener() {
261+
HttpFilter routerFilter = HttpFilter.newBuilder()
262+
.setName("terminal-filter")
263+
.setTypedConfig(
264+
Any.pack(Router.newBuilder().build()))
265+
.setIsOptional(true)
266+
.build();
267+
VirtualHost virtualHost = io.envoyproxy.envoy.config.route.v3.VirtualHost.newBuilder()
268+
.setName("virtual-host-0")
269+
.addDomains("*")
270+
.addRoutes(
271+
Route.newBuilder()
272+
.setMatch(
273+
RouteMatch.newBuilder().setPrefix("/").build())
274+
.setNonForwardingAction(NonForwardingAction.newBuilder().build())
275+
.build()).build();
276+
RouteConfiguration routeConfig = RouteConfiguration.newBuilder()
277+
.addVirtualHosts(virtualHost)
278+
.build();
279+
io.envoyproxy.envoy.config.listener.v3.Filter filter = Filter.newBuilder()
280+
.setName("network-filter-0")
281+
.setTypedConfig(
282+
Any.pack(
283+
HttpConnectionManager.newBuilder()
284+
.setRouteConfig(routeConfig)
285+
.addAllHttpFilters(Collections.singletonList(routerFilter))
286+
.build())).build();
287+
FilterChainMatch filterChainMatch = FilterChainMatch.newBuilder()
288+
.setSourceType(FilterChainMatch.ConnectionSourceType.ANY)
289+
.build();
290+
FilterChain filterChain = FilterChain.newBuilder()
291+
.setName("filter-chain-0")
292+
.setFilterChainMatch(filterChainMatch)
293+
.addFilters(filter)
294+
.build();
295+
return Listener.newBuilder()
296+
.setName(SERVER_LISTENER_TEMPLATE_NO_REPLACEMENT)
297+
.setTrafficDirection(TrafficDirection.INBOUND)
298+
.addFilterChains(filterChain)
299+
.build();
300+
}
301+
}

0 commit comments

Comments
 (0)