async-grpc-xds 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/getting-started.md +363 -0
- data/context/index.yaml +14 -0
- data/lib/async/grpc/xds/cluster.rb +5 -5
- data/lib/async/grpc/xds/cluster_discovery_service.rb +56 -0
- data/lib/async/grpc/xds/config_source.rb +46 -0
- data/lib/async/grpc/xds/control_plane.rb +3 -3
- data/lib/async/grpc/xds/discovery_service.rb +69 -0
- data/lib/async/grpc/xds/endpoint_discovery_service.rb +56 -0
- data/lib/async/grpc/xds/server.rb +8 -2
- data/lib/async/grpc/xds/service.rb +5 -103
- data/lib/async/grpc/xds/stream.rb +102 -0
- data/lib/async/grpc/xds/version.rb +1 -1
- data/lib/async/grpc/xds.rb +5 -0
- data/proto/readme.md +8 -24
- data/readme.md +9 -6
- data/releases.md +5 -0
- data.tar.gz.sig +0 -0
- metadata +10 -14
- metadata.gz.sig +3 -1
- data/fixtures/async/grpc/test_interface.rb +0 -79
- data/fixtures/async/grpc/test_message.rb +0 -56
- data/xds/Dockerfile.backend +0 -24
- data/xds/Dockerfile.control-plane +0 -22
- data/xds/backend_server.rb +0 -68
- data/xds/docker-compose.yaml +0 -89
- data/xds/go.mod +0 -22
- data/xds/go.sum +0 -82
- data/xds/readme.md +0 -95
- data/xds/test/async/grpc/xds/client.rb +0 -294
- data/xds/test/async/grpc/xds/control_plane.rb +0 -93
- data/xds/test_server.go +0 -355
- data/xds/update_protos.sh +0 -123
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3f5ca6a95f124eec9df174142e956b1e5c619e22258ed905202526042e3519a9
|
|
4
|
+
data.tar.gz: af26002e1170beff3b9ba228d12f21f0c1fc6893d8d8c5cf3da30b1af3bced3e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d906ffc9d3874ae1430cf345ee0ec7cc6455266b55743e6fdaa9d9893c923188bb77612dfa8dd7e021940ba586bd06a3554d29dd0561ea3c6dbdb67a0629c0a5
|
|
7
|
+
data.tar.gz: 37aa45338885d4ed19a0f76d9c81da3c95391e694e9d6919d5cb8f00f0137e575bc982e0e72ca85a2bbe660cb1d7dd0e8d3f65ef861260bee54741087bba8b51
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
This guide explains how to use `async-grpc-xds` to publish CDS and EDS resources to Envoy, or to discover gRPC backends from Ruby using xDS.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add the gem to your project:
|
|
8
|
+
|
|
9
|
+
~~~ bash
|
|
10
|
+
$ bundle add async-grpc-xds
|
|
11
|
+
~~~
|
|
12
|
+
|
|
13
|
+
The gem provides both sides of an xDS integration:
|
|
14
|
+
|
|
15
|
+
- A control-plane server which publishes in-memory resources to Envoy over ADS or dedicated discovery services.
|
|
16
|
+
- An experimental Ruby gRPC client which discovers clusters and endpoints from an ADS server.
|
|
17
|
+
|
|
18
|
+
## Core Concepts
|
|
19
|
+
|
|
20
|
+
xDS separates logical proxy configuration from the concrete destinations which currently provide it:
|
|
21
|
+
|
|
22
|
+
| API | Resource | Purpose |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| CDS | `Cluster` | Describes an upstream service, protocol, health checks, load-balancing policy, and how to discover its endpoints. |
|
|
25
|
+
| EDS | `ClusterLoadAssignment` | Supplies the concrete IP or Unix-socket endpoints for a cluster. |
|
|
26
|
+
| RDS | `RouteConfiguration` | Maps incoming requests to clusters. |
|
|
27
|
+
| LDS | `Listener` | Describes the addresses and network filters where Envoy accepts traffic. |
|
|
28
|
+
|
|
29
|
+
`async-grpc-xds` currently builds and serves CDS and EDS resources. Routes and listeners normally remain in Envoy's bootstrap configuration or come from another control plane.
|
|
30
|
+
|
|
31
|
+
Three names connect the configuration:
|
|
32
|
+
|
|
33
|
+
- The **management cluster** is a static Envoy cluster, such as `xds_cluster`, which reaches the Ruby xDS server.
|
|
34
|
+
- The **application cluster** is the logical upstream service, such as `application`.
|
|
35
|
+
- The EDS `service_name` and `ClusterLoadAssignment#cluster_name` identify the endpoint assignment used by that application cluster. They default to the application cluster name.
|
|
36
|
+
|
|
37
|
+
The following diagram shows the dedicated CDS and EDS arrangement used throughout this guide:
|
|
38
|
+
|
|
39
|
+
``` mermaid
|
|
40
|
+
flowchart LR
|
|
41
|
+
Bootstrap[Envoy bootstrap] --> Management[xds_cluster]
|
|
42
|
+
Management -->|CDS stream| ControlPlane[Ruby control plane]
|
|
43
|
+
Management -->|EDS stream| ControlPlane
|
|
44
|
+
ControlPlane -->|Cluster| Application[application cluster]
|
|
45
|
+
ControlPlane -->|ClusterLoadAssignment| Workers[worker endpoints]
|
|
46
|
+
Traffic[Application traffic] --> Envoy
|
|
47
|
+
Envoy --> Application
|
|
48
|
+
Application --> Workers
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Serving Dedicated CDS and EDS
|
|
52
|
+
|
|
53
|
+
Dedicated discovery services are a good fit when this control plane owns application clusters and their local workers, but should not claim Envoy's single ADS connection. Another control plane can then use ADS for coordinated listener, route, or other configuration.
|
|
54
|
+
|
|
55
|
+
Create a control plane, publish an application cluster and its initial workers, then serve the dedicated CDS and EDS interfaces:
|
|
56
|
+
|
|
57
|
+
``` ruby
|
|
58
|
+
require "async"
|
|
59
|
+
require "async/grpc/xds"
|
|
60
|
+
require "async/http/endpoint"
|
|
61
|
+
|
|
62
|
+
control_plane = Async::GRPC::XDS::ControlPlane.new(identifier: "application-supervisor")
|
|
63
|
+
eds_config = Async::GRPC::XDS::ConfigSource.grpc("xds_cluster")
|
|
64
|
+
health_check = Async::GRPC::XDS::HTTPHealthCheck.build(
|
|
65
|
+
"/health",
|
|
66
|
+
interval: 2,
|
|
67
|
+
timeout: 1
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
control_plane.update_cluster(
|
|
71
|
+
"application",
|
|
72
|
+
protocol: :http1,
|
|
73
|
+
eds_config: eds_config,
|
|
74
|
+
health_checks: [health_check]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
control_plane.update_endpoints("application", [
|
|
78
|
+
{
|
|
79
|
+
hostname: "worker-1",
|
|
80
|
+
addresses: [{address: "127.0.0.1", port: 9292}],
|
|
81
|
+
healthy: true,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
hostname: "worker-2",
|
|
85
|
+
addresses: [{address: "127.0.0.1", port: 9293}],
|
|
86
|
+
healthy: true,
|
|
87
|
+
},
|
|
88
|
+
])
|
|
89
|
+
|
|
90
|
+
server = Async::GRPC::XDS::Server.new(
|
|
91
|
+
control_plane,
|
|
92
|
+
services: [
|
|
93
|
+
Async::GRPC::XDS::ClusterDiscoveryService,
|
|
94
|
+
Async::GRPC::XDS::EndpointDiscoveryService,
|
|
95
|
+
]
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
endpoint = Async::HTTP::Endpoint.parse(
|
|
99
|
+
"http://0.0.0.0:18000",
|
|
100
|
+
protocol: Async::HTTP::Protocol::HTTP2
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
Sync do
|
|
104
|
+
server.run(endpoint)
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The management endpoint uses HTTP/2 because xDS is a gRPC protocol. This example uses plaintext HTTP/2 within a trusted local network; deployments can instead configure TLS on the endpoint.
|
|
109
|
+
|
|
110
|
+
### Envoy Bootstrap
|
|
111
|
+
|
|
112
|
+
Envoy must know how to reach the management server before it can discover anything else. Define `xds_cluster` statically and configure CDS to use it:
|
|
113
|
+
|
|
114
|
+
``` yaml
|
|
115
|
+
node:
|
|
116
|
+
id: application-proxy-1
|
|
117
|
+
cluster: application-proxies
|
|
118
|
+
|
|
119
|
+
admin:
|
|
120
|
+
address:
|
|
121
|
+
socket_address:
|
|
122
|
+
address: 127.0.0.1
|
|
123
|
+
port_value: 19000
|
|
124
|
+
|
|
125
|
+
dynamic_resources:
|
|
126
|
+
cds_config:
|
|
127
|
+
resource_api_version: V3
|
|
128
|
+
api_config_source:
|
|
129
|
+
api_type: GRPC
|
|
130
|
+
transport_api_version: V3
|
|
131
|
+
grpc_services:
|
|
132
|
+
- envoy_grpc:
|
|
133
|
+
cluster_name: xds_cluster
|
|
134
|
+
|
|
135
|
+
static_resources:
|
|
136
|
+
listeners:
|
|
137
|
+
- name: ingress
|
|
138
|
+
address:
|
|
139
|
+
socket_address:
|
|
140
|
+
address: 0.0.0.0
|
|
141
|
+
port_value: 8080
|
|
142
|
+
filter_chains:
|
|
143
|
+
- filters:
|
|
144
|
+
- name: envoy.filters.network.http_connection_manager
|
|
145
|
+
typed_config:
|
|
146
|
+
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
|
|
147
|
+
stat_prefix: ingress_http
|
|
148
|
+
route_config:
|
|
149
|
+
name: local_route
|
|
150
|
+
validate_clusters: false
|
|
151
|
+
virtual_hosts:
|
|
152
|
+
- name: application
|
|
153
|
+
domains: ["*"]
|
|
154
|
+
routes:
|
|
155
|
+
- match:
|
|
156
|
+
prefix: "/"
|
|
157
|
+
route:
|
|
158
|
+
cluster: application
|
|
159
|
+
http_filters:
|
|
160
|
+
- name: envoy.filters.http.router
|
|
161
|
+
typed_config:
|
|
162
|
+
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
|
|
163
|
+
|
|
164
|
+
clusters:
|
|
165
|
+
- name: xds_cluster
|
|
166
|
+
connect_timeout: 1s
|
|
167
|
+
type: STATIC
|
|
168
|
+
typed_extension_protocol_options:
|
|
169
|
+
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
|
|
170
|
+
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
|
|
171
|
+
explicit_http_config:
|
|
172
|
+
http2_protocol_options: {}
|
|
173
|
+
load_assignment:
|
|
174
|
+
cluster_name: xds_cluster
|
|
175
|
+
endpoints:
|
|
176
|
+
- lb_endpoints:
|
|
177
|
+
- endpoint:
|
|
178
|
+
address:
|
|
179
|
+
socket_address:
|
|
180
|
+
address: 127.0.0.1
|
|
181
|
+
port_value: 18000
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The static route names `application` before CDS has delivered that cluster, so `validate_clusters: false` allows the bootstrap configuration to load. Envoy keeps the discovered cluster warming until its EDS assignment arrives.
|
|
185
|
+
|
|
186
|
+
The `xds_cluster` name must match the name passed to {ruby Async::GRPC::XDS::ConfigSource.grpc}. The generated application cluster then opens its dedicated EDS stream through the same management cluster.
|
|
187
|
+
|
|
188
|
+
## Publishing Endpoint Changes
|
|
189
|
+
|
|
190
|
+
The control plane is thread-safe and immediately notifies connected discovery streams after a resource changes. Publish the complete current endpoint assignment whenever workers start, stop, or change health:
|
|
191
|
+
|
|
192
|
+
``` ruby
|
|
193
|
+
control_plane.update_endpoints("application", [
|
|
194
|
+
{
|
|
195
|
+
hostname: "worker-2",
|
|
196
|
+
addresses: [{address: "127.0.0.1", port: 9293}],
|
|
197
|
+
healthy: true,
|
|
198
|
+
},
|
|
199
|
+
])
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
This implementation uses state-of-the-world discovery: each EDS response contains the complete assignment requested by Envoy. It does not currently implement the delta discovery RPCs.
|
|
203
|
+
|
|
204
|
+
Every call to `update_cluster` or `update_endpoints` increments the version for that resource type, even if the generated resource is unchanged. Producers should therefore avoid publishing redundant updates.
|
|
205
|
+
|
|
206
|
+
To remove a service completely, remove both resources:
|
|
207
|
+
|
|
208
|
+
``` ruby
|
|
209
|
+
control_plane.remove_endpoints("application")
|
|
210
|
+
control_plane.remove_cluster("application")
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### IP and Unix-Socket Addresses
|
|
214
|
+
|
|
215
|
+
An endpoint must contain one or more addresses. An IP address uses `:address` and `:port`:
|
|
216
|
+
|
|
217
|
+
``` ruby
|
|
218
|
+
{
|
|
219
|
+
addresses: [{address: "127.0.0.1", port: 9292}],
|
|
220
|
+
healthy: true,
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
A Unix domain socket uses `:path`:
|
|
225
|
+
|
|
226
|
+
``` ruby
|
|
227
|
+
{
|
|
228
|
+
addresses: [{path: "/run/application/worker-1.ipc"}],
|
|
229
|
+
healthy: true,
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Several addresses in one `:addresses` array describe alternative addresses for one logical load-balancer endpoint. The first becomes Envoy's primary address and the remainder become `additional_addresses`; they do not represent additional workers.
|
|
234
|
+
|
|
235
|
+
### Health
|
|
236
|
+
|
|
237
|
+
The `:healthy` value sets the EDS `health_status` for an endpoint. It accepts healthy, unhealthy, degraded, or unknown states; booleans map to healthy and unhealthy.
|
|
238
|
+
|
|
239
|
+
This reported state is separate from active health checks. Adding a health check to the cluster tells Envoy to probe every published endpoint itself:
|
|
240
|
+
|
|
241
|
+
``` ruby
|
|
242
|
+
health_check = Async::GRPC::XDS::HTTPHealthCheck.build(
|
|
243
|
+
"/health",
|
|
244
|
+
interval: 2,
|
|
245
|
+
timeout: 1,
|
|
246
|
+
unhealthy_threshold: 2,
|
|
247
|
+
healthy_threshold: 1
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
control_plane.update_cluster(
|
|
251
|
+
"application",
|
|
252
|
+
protocol: :http1,
|
|
253
|
+
eds_config: Async::GRPC::XDS::ConfigSource.grpc("xds_cluster"),
|
|
254
|
+
health_checks: [health_check]
|
|
255
|
+
)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Use reported health for information already known by the resource owner, such as whether a worker remains registered. Use active health checks when Envoy should independently verify that it can send application traffic to the endpoint.
|
|
259
|
+
|
|
260
|
+
## ADS or Dedicated Services
|
|
261
|
+
|
|
262
|
+
Both transports carry the same `DiscoveryRequest` and `DiscoveryResponse` messages. The difference is how Envoy organizes its streams and management servers:
|
|
263
|
+
|
|
264
|
+
| | ADS | Dedicated CDS and EDS |
|
|
265
|
+
| --- | --- | --- |
|
|
266
|
+
| Streams | One aggregated stream for several resource types. | One stream per resource type. |
|
|
267
|
+
| Coordination | Provides ordering across related resource types from one control plane. | Each resource type progresses independently. |
|
|
268
|
+
| Ownership | Envoy has one ADS management server. | Each resource type can use its own configuration source. |
|
|
269
|
+
| Best fit | One control plane owns coordinated proxy configuration. | A focused control plane owns only clusters or endpoints. |
|
|
270
|
+
|
|
271
|
+
The default server exposes ADS:
|
|
272
|
+
|
|
273
|
+
``` ruby
|
|
274
|
+
control_plane = Async::GRPC::XDS::ControlPlane.new
|
|
275
|
+
control_plane.update_cluster("application", protocol: :http1)
|
|
276
|
+
control_plane.update_endpoints("application", endpoints)
|
|
277
|
+
|
|
278
|
+
server = Async::GRPC::XDS::Server.new(control_plane)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
{ruby Async::GRPC::XDS::Cluster.build} uses ADS for EDS by default, so no explicit `eds_config` is needed in this mode. Configure Envoy accordingly:
|
|
282
|
+
|
|
283
|
+
``` yaml
|
|
284
|
+
dynamic_resources:
|
|
285
|
+
ads_config:
|
|
286
|
+
api_type: GRPC
|
|
287
|
+
transport_api_version: V3
|
|
288
|
+
grpc_services:
|
|
289
|
+
- envoy_grpc:
|
|
290
|
+
cluster_name: xds_cluster
|
|
291
|
+
cds_config:
|
|
292
|
+
resource_api_version: V3
|
|
293
|
+
ads: {}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Do not use the default ADS EDS source when serving dedicated CDS and EDS. In that arrangement, pass `ConfigSource.grpc("xds_cluster")` while building or updating every application cluster.
|
|
297
|
+
|
|
298
|
+
## Using the Ruby xDS Client
|
|
299
|
+
|
|
300
|
+
{ruby Async::GRPC::XDS::Client} is an experimental gRPC client which discovers a named cluster and its healthy endpoints over ADS, then load-balances RPC calls between them.
|
|
301
|
+
|
|
302
|
+
Supply bootstrap configuration directly:
|
|
303
|
+
|
|
304
|
+
``` ruby
|
|
305
|
+
bootstrap = {
|
|
306
|
+
xds_servers: [
|
|
307
|
+
{
|
|
308
|
+
server_uri: "127.0.0.1:18000",
|
|
309
|
+
channel_creds: [{type: "insecure"}],
|
|
310
|
+
}
|
|
311
|
+
],
|
|
312
|
+
node: {
|
|
313
|
+
id: "orders-client-1",
|
|
314
|
+
cluster: "orders-clients",
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
Sync do
|
|
319
|
+
client = Async::GRPC::XDS::Client.new("application", bootstrap: bootstrap)
|
|
320
|
+
|
|
321
|
+
begin
|
|
322
|
+
stub = client.stub(Greeter::Interface, "example.Greeter")
|
|
323
|
+
response = stub.say_hello(Greeter::Request.new(name: "World"))
|
|
324
|
+
ensure
|
|
325
|
+
client.close
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Alternatively, pass the path to a JSON bootstrap file. When `bootstrap` is omitted, the client checks `GRPC_XDS_BOOTSTRAP` and then `~/.config/grpc/bootstrap.json`.
|
|
331
|
+
|
|
332
|
+
The Ruby client currently consumes ADS, supports CDS and EDS, filters endpoints by reported health, and provides basic client-side load balancing and retry behavior. Dedicated CDS and EDS client streams are not yet implemented.
|
|
333
|
+
|
|
334
|
+
## Operational Checks
|
|
335
|
+
|
|
336
|
+
Envoy's admin interface shows whether it accepted the resources and whether the application cluster has usable members:
|
|
337
|
+
|
|
338
|
+
~~~ bash
|
|
339
|
+
$ curl -s http://127.0.0.1:19000/config_dump
|
|
340
|
+
$ curl -s http://127.0.0.1:19000/clusters
|
|
341
|
+
$ curl -s http://127.0.0.1:19000/stats | grep -E 'cluster\.application\.(warming|membership_healthy|update_rejected)'
|
|
342
|
+
~~~
|
|
343
|
+
|
|
344
|
+
When a cluster remains in `warming`, check:
|
|
345
|
+
|
|
346
|
+
- The `xds_cluster` address and port reach the Ruby server using HTTP/2.
|
|
347
|
+
- The management cluster name matches the name in every gRPC configuration source.
|
|
348
|
+
- The CDS cluster name, EDS service name, and `ClusterLoadAssignment#cluster_name` agree.
|
|
349
|
+
- A dedicated CDS cluster uses a dedicated EDS configuration source rather than `ads: {}`.
|
|
350
|
+
- Envoy has not incremented `update_rejected`; rejected responses are also returned to the control plane as NACKs and logged.
|
|
351
|
+
|
|
352
|
+
## Current Scope
|
|
353
|
+
|
|
354
|
+
`async-grpc-xds` currently provides:
|
|
355
|
+
|
|
356
|
+
- xDS v3 protobuf definitions.
|
|
357
|
+
- State-of-the-world ADS, CDS, and EDS server streams.
|
|
358
|
+
- In-memory cluster and endpoint resources with per-type versions.
|
|
359
|
+
- HTTP/1 and HTTP/2 upstream cluster configuration.
|
|
360
|
+
- IP, Unix-socket, health-status, active HTTP health-check, and out-of-band ORCA policy resource builders.
|
|
361
|
+
- An experimental ADS-based Ruby gRPC client.
|
|
362
|
+
|
|
363
|
+
Delta discovery, complete NACK recovery, persistent resource storage, LDS/RDS serving, locality weighting, and complete routing semantics are not implemented yet.
|
data/context/index.yaml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Automatically generated context index for Utopia::Project guides.
|
|
2
|
+
# Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`.
|
|
3
|
+
---
|
|
4
|
+
description: xDS support for Async::GRPC clients.
|
|
5
|
+
metadata:
|
|
6
|
+
bug_tracker_uri: https://github.com/socketry/async-grpc-xds/issues
|
|
7
|
+
changelog_uri: https://github.com/socketry/async-grpc-xds/blob/main/releases.md
|
|
8
|
+
documentation_uri: https://socketry.github.io/async-grpc-xds/
|
|
9
|
+
source_code_uri: https://github.com/socketry/async-grpc-xds.git
|
|
10
|
+
files:
|
|
11
|
+
- path: getting-started.md
|
|
12
|
+
title: Getting Started
|
|
13
|
+
description: This guide explains how to use `async-grpc-xds` to publish CDS and
|
|
14
|
+
EDS resources to Envoy, or to discover gRPC backends from Ruby using xDS.
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
require "google/protobuf/duration_pb"
|
|
7
7
|
|
|
8
8
|
require "envoy/config/cluster/v3/cluster_pb"
|
|
9
|
-
require "envoy/config/core/v3/config_source_pb"
|
|
10
9
|
require "envoy/config/core/v3/protocol_pb"
|
|
11
10
|
|
|
11
|
+
require_relative "config_source"
|
|
12
|
+
|
|
12
13
|
module Async
|
|
13
14
|
module GRPC
|
|
14
15
|
module XDS
|
|
@@ -21,21 +22,20 @@ module Async
|
|
|
21
22
|
# Build an EDS cluster resource.
|
|
22
23
|
# @parameter name [String] The cluster name.
|
|
23
24
|
# @parameter service_name [String] The EDS service name.
|
|
25
|
+
# @parameter eds_config [Envoy::Config::Core::V3::ConfigSource] The source used to discover endpoint assignments.
|
|
24
26
|
# @parameter load_balancing_policy [Envoy::Config::Cluster::V3::LoadBalancingPolicy | Nil] The typed Envoy load-balancing policy.
|
|
25
27
|
# @parameter health_checks [Array(Envoy::Config::Core::V3::HealthCheck)] The active health checks applied to cluster endpoints.
|
|
26
28
|
# @parameter connect_timeout [Numeric] The upstream connection timeout in seconds.
|
|
27
29
|
# @parameter protocol [Symbol] The canonical upstream protocol, either `:http1` or `:http2`.
|
|
28
30
|
# @returns [Envoy::Config::Cluster::V3::Cluster] The generated cluster resource.
|
|
29
31
|
# @raises [ArgumentError] If the upstream protocol is unsupported.
|
|
30
|
-
def build(name, service_name: name, load_balancing_policy: nil, health_checks: [], connect_timeout: 5, protocol: :http2)
|
|
32
|
+
def build(name, service_name: name, eds_config: ConfigSource.ads, load_balancing_policy: nil, health_checks: [], connect_timeout: 5, protocol: :http2)
|
|
31
33
|
options = {
|
|
32
34
|
name: name.to_s,
|
|
33
35
|
type: Envoy::Config::Cluster::V3::Cluster::DiscoveryType::EDS,
|
|
34
36
|
eds_cluster_config: Envoy::Config::Cluster::V3::Cluster::EdsClusterConfig.new(
|
|
35
37
|
service_name: service_name.to_s,
|
|
36
|
-
eds_config:
|
|
37
|
-
ads: Envoy::Config::Core::V3::AggregatedConfigSource.new
|
|
38
|
-
)
|
|
38
|
+
eds_config: eds_config
|
|
39
39
|
),
|
|
40
40
|
connect_timeout: duration(connect_timeout),
|
|
41
41
|
health_checks: health_checks,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "envoy/service/discovery/v3/discovery_pb"
|
|
7
|
+
require "protocol/grpc/interface"
|
|
8
|
+
|
|
9
|
+
require_relative "control_plane"
|
|
10
|
+
require_relative "discovery_service"
|
|
11
|
+
|
|
12
|
+
module Async
|
|
13
|
+
module GRPC
|
|
14
|
+
module XDS
|
|
15
|
+
# Serves Cluster Discovery Service requests from a {ControlPlane}.
|
|
16
|
+
class ClusterDiscoveryService < DiscoveryService
|
|
17
|
+
SERVICE_NAME = "envoy.service.cluster.v3.ClusterDiscoveryService"
|
|
18
|
+
RESOURCE_TYPE = ControlPlane::CLUSTER_TYPE
|
|
19
|
+
|
|
20
|
+
# The gRPC interface for cluster discovery.
|
|
21
|
+
class Interface < Protocol::GRPC::Interface
|
|
22
|
+
rpc :StreamClusters,
|
|
23
|
+
request_class: Envoy::Service::Discovery::V3::DiscoveryRequest,
|
|
24
|
+
response_class: Envoy::Service::Discovery::V3::DiscoveryResponse,
|
|
25
|
+
streaming: :bidirectional
|
|
26
|
+
|
|
27
|
+
rpc :DeltaClusters,
|
|
28
|
+
request_class: Envoy::Service::Discovery::V3::DeltaDiscoveryRequest,
|
|
29
|
+
response_class: Envoy::Service::Discovery::V3::DeltaDiscoveryResponse,
|
|
30
|
+
streaming: :bidirectional
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Initialize a Cluster Discovery Service.
|
|
34
|
+
# @parameter control_plane [ControlPlane] The control plane that provides clusters.
|
|
35
|
+
def initialize(control_plane)
|
|
36
|
+
super(Interface, SERVICE_NAME, control_plane, resource_type: RESOURCE_TYPE)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Serve a state-of-the-world cluster discovery stream.
|
|
40
|
+
# @parameter input [Enumerable] The stream of discovery requests.
|
|
41
|
+
# @parameter output [Interface(:write)] The discovery response stream.
|
|
42
|
+
# @parameter call [Object] The gRPC call context.
|
|
43
|
+
# @asynchronous
|
|
44
|
+
def stream_clusters(input, output, call)
|
|
45
|
+
stream_resources(input, output)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Reject a delta cluster discovery stream, which is not supported.
|
|
49
|
+
# @raises [Protocol::GRPC::Error] Always raised because delta xDS is not implemented.
|
|
50
|
+
def delta_clusters(input, output, call)
|
|
51
|
+
delta_resources
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "envoy/config/core/v3/config_source_pb"
|
|
7
|
+
require "envoy/config/core/v3/grpc_service_pb"
|
|
8
|
+
|
|
9
|
+
module Async
|
|
10
|
+
module GRPC
|
|
11
|
+
module XDS
|
|
12
|
+
# Builds Envoy xDS configuration sources.
|
|
13
|
+
module ConfigSource
|
|
14
|
+
extend self
|
|
15
|
+
|
|
16
|
+
# Build a configuration source that uses the global ADS server.
|
|
17
|
+
# @returns [Envoy::Config::Core::V3::ConfigSource] The ADS configuration source.
|
|
18
|
+
def ads
|
|
19
|
+
Envoy::Config::Core::V3::ConfigSource.new(
|
|
20
|
+
ads: Envoy::Config::Core::V3::AggregatedConfigSource.new
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Build a configuration source for a dedicated gRPC discovery service.
|
|
25
|
+
# @parameter cluster_name [String] The static Envoy cluster used to reach the management server.
|
|
26
|
+
# @returns [Envoy::Config::Core::V3::ConfigSource] The gRPC API configuration source.
|
|
27
|
+
def grpc(cluster_name)
|
|
28
|
+
Envoy::Config::Core::V3::ConfigSource.new(
|
|
29
|
+
resource_api_version: :V3,
|
|
30
|
+
api_config_source: Envoy::Config::Core::V3::ApiConfigSource.new(
|
|
31
|
+
api_type: :GRPC,
|
|
32
|
+
transport_api_version: :V3,
|
|
33
|
+
grpc_services: [
|
|
34
|
+
Envoy::Config::Core::V3::GrpcService.new(
|
|
35
|
+
envoy_grpc: Envoy::Config::Core::V3::GrpcService::EnvoyGrpc.new(
|
|
36
|
+
cluster_name: cluster_name.to_s
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
]
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -19,7 +19,7 @@ require_relative "endpoint"
|
|
|
19
19
|
module Async
|
|
20
20
|
module GRPC
|
|
21
21
|
module XDS
|
|
22
|
-
# Maintains xDS resource snapshots and notifies
|
|
22
|
+
# Maintains xDS resource snapshots and notifies discovery streams when resources change.
|
|
23
23
|
class ControlPlane
|
|
24
24
|
CLUSTER_TYPE = Cluster::TYPE_URL
|
|
25
25
|
ENDPOINT_TYPE = Endpoint::TYPE_URL
|
|
@@ -152,7 +152,7 @@ module Async
|
|
|
152
152
|
end
|
|
153
153
|
|
|
154
154
|
# Register a stream to receive resource-change notifications.
|
|
155
|
-
# @parameter stream [
|
|
155
|
+
# @parameter stream [Stream] The stream to register.
|
|
156
156
|
def register_stream(stream)
|
|
157
157
|
@mutex.synchronize do
|
|
158
158
|
@streams.add(stream)
|
|
@@ -160,7 +160,7 @@ module Async
|
|
|
160
160
|
end
|
|
161
161
|
|
|
162
162
|
# Remove a registered stream.
|
|
163
|
-
# @parameter stream [
|
|
163
|
+
# @parameter stream [Stream] The stream to remove.
|
|
164
164
|
def remove_stream(stream)
|
|
165
165
|
@mutex.synchronize do
|
|
166
166
|
@streams.delete(stream)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "async"
|
|
7
|
+
require "async/grpc/service"
|
|
8
|
+
require "protocol/grpc/error"
|
|
9
|
+
require "protocol/grpc/status"
|
|
10
|
+
|
|
11
|
+
require_relative "control_plane"
|
|
12
|
+
require_relative "stream"
|
|
13
|
+
|
|
14
|
+
module Async
|
|
15
|
+
module GRPC
|
|
16
|
+
module XDS
|
|
17
|
+
# Shared implementation for state-of-the-world xDS discovery services.
|
|
18
|
+
class DiscoveryService < Async::GRPC::Service
|
|
19
|
+
# Initialize a discovery service.
|
|
20
|
+
# @parameter interface [Class] The gRPC service interface.
|
|
21
|
+
# @parameter service_name [String] The fully qualified gRPC service name.
|
|
22
|
+
# @parameter control_plane [ControlPlane] The control plane that provides resources.
|
|
23
|
+
# @parameter resource_type [String | Nil] The fixed resource type, or `nil` for aggregated discovery.
|
|
24
|
+
def initialize(interface, service_name, control_plane, resource_type: nil)
|
|
25
|
+
super(interface, service_name)
|
|
26
|
+
|
|
27
|
+
@control_plane = control_plane
|
|
28
|
+
@resource_type = resource_type
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Serve a state-of-the-world discovery stream.
|
|
32
|
+
# @parameter input [Enumerable] The stream of discovery requests.
|
|
33
|
+
# @parameter output [Interface(:write)] The discovery response stream.
|
|
34
|
+
# @asynchronous
|
|
35
|
+
def stream_resources(input, output)
|
|
36
|
+
stream = Stream.new(@control_plane, output, resource_type: @resource_type)
|
|
37
|
+
@control_plane.register_stream(stream)
|
|
38
|
+
|
|
39
|
+
reader = Async::Task.current.async do
|
|
40
|
+
input.each do |request|
|
|
41
|
+
stream.request(request)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
writer = Async::Task.current.async do
|
|
46
|
+
stream.run
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
reader.wait
|
|
50
|
+
ensure
|
|
51
|
+
stream&.close
|
|
52
|
+
reader&.stop
|
|
53
|
+
writer&.stop
|
|
54
|
+
@control_plane.remove_stream(stream) if stream
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Reject a delta discovery stream, which is not supported.
|
|
58
|
+
# @raises [Protocol::GRPC::Error] Always raised because delta xDS is not implemented.
|
|
59
|
+
def delta_resources
|
|
60
|
+
raise Protocol::GRPC::Error.new(
|
|
61
|
+
Protocol::GRPC::Status::UNIMPLEMENTED,
|
|
62
|
+
"Delta xDS is not implemented."
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "envoy/service/discovery/v3/discovery_pb"
|
|
7
|
+
require "protocol/grpc/interface"
|
|
8
|
+
|
|
9
|
+
require_relative "control_plane"
|
|
10
|
+
require_relative "discovery_service"
|
|
11
|
+
|
|
12
|
+
module Async
|
|
13
|
+
module GRPC
|
|
14
|
+
module XDS
|
|
15
|
+
# Serves Endpoint Discovery Service requests from a {ControlPlane}.
|
|
16
|
+
class EndpointDiscoveryService < DiscoveryService
|
|
17
|
+
SERVICE_NAME = "envoy.service.endpoint.v3.EndpointDiscoveryService"
|
|
18
|
+
RESOURCE_TYPE = ControlPlane::ENDPOINT_TYPE
|
|
19
|
+
|
|
20
|
+
# The gRPC interface for endpoint discovery.
|
|
21
|
+
class Interface < Protocol::GRPC::Interface
|
|
22
|
+
rpc :StreamEndpoints,
|
|
23
|
+
request_class: Envoy::Service::Discovery::V3::DiscoveryRequest,
|
|
24
|
+
response_class: Envoy::Service::Discovery::V3::DiscoveryResponse,
|
|
25
|
+
streaming: :bidirectional
|
|
26
|
+
|
|
27
|
+
rpc :DeltaEndpoints,
|
|
28
|
+
request_class: Envoy::Service::Discovery::V3::DeltaDiscoveryRequest,
|
|
29
|
+
response_class: Envoy::Service::Discovery::V3::DeltaDiscoveryResponse,
|
|
30
|
+
streaming: :bidirectional
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Initialize an Endpoint Discovery Service.
|
|
34
|
+
# @parameter control_plane [ControlPlane] The control plane that provides endpoint assignments.
|
|
35
|
+
def initialize(control_plane)
|
|
36
|
+
super(Interface, SERVICE_NAME, control_plane, resource_type: RESOURCE_TYPE)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Serve a state-of-the-world endpoint discovery stream.
|
|
40
|
+
# @parameter input [Enumerable] The stream of discovery requests.
|
|
41
|
+
# @parameter output [Interface(:write)] The discovery response stream.
|
|
42
|
+
# @parameter call [Object] The gRPC call context.
|
|
43
|
+
# @asynchronous
|
|
44
|
+
def stream_endpoints(input, output, call)
|
|
45
|
+
stream_resources(input, output)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Reject a delta endpoint discovery stream, which is not supported.
|
|
49
|
+
# @raises [Protocol::GRPC::Error] Always raised because delta xDS is not implemented.
|
|
50
|
+
def delta_endpoints(input, output, call)
|
|
51
|
+
delta_resources
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|