async-grpc-xds 0.0.1

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.
Files changed (239) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/fixtures/async/grpc/test_interface.rb +79 -0
  4. data/fixtures/async/grpc/test_message.rb +56 -0
  5. data/lib/async/grpc/xds/ads_stream.rb +70 -0
  6. data/lib/async/grpc/xds/client.rb +255 -0
  7. data/lib/async/grpc/xds/context.rb +201 -0
  8. data/lib/async/grpc/xds/control_plane.rb +143 -0
  9. data/lib/async/grpc/xds/discovery_client.rb +356 -0
  10. data/lib/async/grpc/xds/health_checker.rb +88 -0
  11. data/lib/async/grpc/xds/load_balancer.rb +196 -0
  12. data/lib/async/grpc/xds/resource_builder.rb +138 -0
  13. data/lib/async/grpc/xds/resource_cache.rb +55 -0
  14. data/lib/async/grpc/xds/resources.rb +270 -0
  15. data/lib/async/grpc/xds/server.rb +34 -0
  16. data/lib/async/grpc/xds/service.rb +117 -0
  17. data/lib/async/grpc/xds/version.rb +12 -0
  18. data/lib/async/grpc/xds.rb +42 -0
  19. data/lib/envoy/annotations/deprecation_pb.rb +19 -0
  20. data/lib/envoy/config/cluster/v3/circuit_breaker_pb.rb +31 -0
  21. data/lib/envoy/config/cluster/v3/cluster_pb.rb +80 -0
  22. data/lib/envoy/config/cluster/v3/filter_pb.rb +28 -0
  23. data/lib/envoy/config/cluster/v3/outlier_detection_pb.rb +29 -0
  24. data/lib/envoy/config/core/v3/address_pb.rb +38 -0
  25. data/lib/envoy/config/core/v3/backoff_pb.rb +27 -0
  26. data/lib/envoy/config/core/v3/base_pb.rb +68 -0
  27. data/lib/envoy/config/core/v3/cel_pb.rb +24 -0
  28. data/lib/envoy/config/core/v3/config_source_pb.rb +42 -0
  29. data/lib/envoy/config/core/v3/event_service_config_pb.rb +27 -0
  30. data/lib/envoy/config/core/v3/extension_pb.rb +26 -0
  31. data/lib/envoy/config/core/v3/grpc_method_list_pb.rb +27 -0
  32. data/lib/envoy/config/core/v3/grpc_service_pb.rb +45 -0
  33. data/lib/envoy/config/core/v3/health_check_pb.rb +47 -0
  34. data/lib/envoy/config/core/v3/http_service_pb.rb +27 -0
  35. data/lib/envoy/config/core/v3/http_uri_pb.rb +27 -0
  36. data/lib/envoy/config/core/v3/protocol_pb.rb +51 -0
  37. data/lib/envoy/config/core/v3/proxy_protocol_pb.rb +31 -0
  38. data/lib/envoy/config/core/v3/resolver_pb.rb +27 -0
  39. data/lib/envoy/config/core/v3/socket_cmsg_headers_pb.rb +25 -0
  40. data/lib/envoy/config/core/v3/socket_option_pb.rb +31 -0
  41. data/lib/envoy/config/core/v3/substitution_format_string_pb.rb +30 -0
  42. data/lib/envoy/config/core/v3/udp_socket_config_pb.rb +26 -0
  43. data/lib/envoy/config/endpoint/v3/endpoint_components_pb.rb +40 -0
  44. data/lib/envoy/config/endpoint/v3/endpoint_pb.rb +32 -0
  45. data/lib/envoy/config/endpoint/v3/load_report_pb.rb +36 -0
  46. data/lib/envoy/service/discovery/v3/ads_pb.rb +26 -0
  47. data/lib/envoy/service/discovery/v3/aggregated_discovery_service.rb +64 -0
  48. data/lib/envoy/service/discovery/v3/discovery_pb.rb +42 -0
  49. data/lib/envoy/type/matcher/v3/address_pb.rb +25 -0
  50. data/lib/envoy/type/matcher/v3/filter_state_pb.rb +27 -0
  51. data/lib/envoy/type/matcher/v3/http_inputs_pb.rb +29 -0
  52. data/lib/envoy/type/matcher/v3/metadata_pb.rb +28 -0
  53. data/lib/envoy/type/matcher/v3/node_pb.rb +27 -0
  54. data/lib/envoy/type/matcher/v3/number_pb.rb +27 -0
  55. data/lib/envoy/type/matcher/v3/path_pb.rb +27 -0
  56. data/lib/envoy/type/matcher/v3/regex_pb.rb +30 -0
  57. data/lib/envoy/type/matcher/v3/status_code_input_pb.rb +25 -0
  58. data/lib/envoy/type/matcher/v3/string_pb.rb +29 -0
  59. data/lib/envoy/type/matcher/v3/struct_pb.rb +28 -0
  60. data/lib/envoy/type/matcher/v3/value_pb.rb +31 -0
  61. data/lib/envoy/type/metadata/v3/metadata_pb.rb +32 -0
  62. data/lib/envoy/type/v3/hash_policy_pb.rb +26 -0
  63. data/lib/envoy/type/v3/http_pb.rb +22 -0
  64. data/lib/envoy/type/v3/http_status_pb.rb +25 -0
  65. data/lib/envoy/type/v3/percent_pb.rb +26 -0
  66. data/lib/envoy/type/v3/range_pb.rb +25 -0
  67. data/lib/envoy/type/v3/ratelimit_strategy_pb.rb +28 -0
  68. data/lib/envoy/type/v3/ratelimit_unit_pb.rb +22 -0
  69. data/lib/envoy/type/v3/semantic_version_pb.rb +23 -0
  70. data/lib/envoy/type/v3/token_bucket_pb.rb +26 -0
  71. data/lib/envoy.rb +83 -0
  72. data/lib/google/protobuf/any_pb.rb +18 -0
  73. data/lib/google/protobuf/duration_pb.rb +18 -0
  74. data/lib/google/protobuf/empty_pb.rb +18 -0
  75. data/lib/google/protobuf/struct_pb.rb +21 -0
  76. data/lib/google/protobuf/timestamp_pb.rb +18 -0
  77. data/lib/google/protobuf/wrappers_pb.rb +26 -0
  78. data/lib/google/rpc/status_pb.rb +20 -0
  79. data/lib/udpa/annotations/migrate_pb.rb +22 -0
  80. data/lib/udpa/annotations/security_pb.rb +23 -0
  81. data/lib/udpa/annotations/sensitive_pb.rb +19 -0
  82. data/lib/udpa/annotations/status_pb.rb +21 -0
  83. data/lib/udpa/annotations/versioning_pb.rb +20 -0
  84. data/lib/validate/validate_pb.rb +43 -0
  85. data/lib/xds/annotations/v3/status_pb.rb +26 -0
  86. data/lib/xds/core/v3/authority_pb.rb +23 -0
  87. data/lib/xds/core/v3/cidr_pb.rb +24 -0
  88. data/lib/xds/core/v3/collection_entry_pb.rb +26 -0
  89. data/lib/xds/core/v3/context_params_pb.rb +22 -0
  90. data/lib/xds/core/v3/extension_pb.rb +23 -0
  91. data/lib/xds/core/v3/resource_locator_pb.rb +26 -0
  92. data/lib/xds/core/v3/resource_name_pb.rb +24 -0
  93. data/lib/xds/core/v3/resource_pb.rb +24 -0
  94. data/lib/xds/type/matcher/v3/domain_pb.rb +27 -0
  95. data/lib/xds/type/matcher/v3/http_inputs_pb.rb +22 -0
  96. data/lib/xds/type/matcher/v3/ip_pb.rb +28 -0
  97. data/lib/xds/type/matcher/v3/matcher_pb.rb +34 -0
  98. data/lib/xds/type/matcher/v3/range_pb.rb +31 -0
  99. data/lib/xds/type/matcher/v3/regex_pb.rb +25 -0
  100. data/lib/xds/type/matcher/v3/string_pb.rb +27 -0
  101. data/license.md +21 -0
  102. data/plan.md +156 -0
  103. data/proto/envoy/annotations/deprecation.proto +34 -0
  104. data/proto/envoy/annotations/resource.proto +19 -0
  105. data/proto/envoy/config/README.md +3 -0
  106. data/proto/envoy/config/cluster/v3/BUILD +18 -0
  107. data/proto/envoy/config/cluster/v3/circuit_breaker.proto +121 -0
  108. data/proto/envoy/config/cluster/v3/cluster.proto +1407 -0
  109. data/proto/envoy/config/cluster/v3/filter.proto +40 -0
  110. data/proto/envoy/config/cluster/v3/outlier_detection.proto +180 -0
  111. data/proto/envoy/config/core/v3/BUILD +16 -0
  112. data/proto/envoy/config/core/v3/address.proto +214 -0
  113. data/proto/envoy/config/core/v3/backoff.proto +37 -0
  114. data/proto/envoy/config/core/v3/base.proto +662 -0
  115. data/proto/envoy/config/core/v3/cel.proto +63 -0
  116. data/proto/envoy/config/core/v3/config_source.proto +283 -0
  117. data/proto/envoy/config/core/v3/event_service_config.proto +29 -0
  118. data/proto/envoy/config/core/v3/extension.proto +32 -0
  119. data/proto/envoy/config/core/v3/grpc_method_list.proto +33 -0
  120. data/proto/envoy/config/core/v3/grpc_service.proto +355 -0
  121. data/proto/envoy/config/core/v3/health_check.proto +443 -0
  122. data/proto/envoy/config/core/v3/http_service.proto +35 -0
  123. data/proto/envoy/config/core/v3/http_uri.proto +58 -0
  124. data/proto/envoy/config/core/v3/protocol.proto +807 -0
  125. data/proto/envoy/config/core/v3/proxy_protocol.proto +114 -0
  126. data/proto/envoy/config/core/v3/resolver.proto +36 -0
  127. data/proto/envoy/config/core/v3/socket_cmsg_headers.proto +28 -0
  128. data/proto/envoy/config/core/v3/socket_option.proto +108 -0
  129. data/proto/envoy/config/core/v3/substitution_format_string.proto +136 -0
  130. data/proto/envoy/config/core/v3/udp_socket_config.proto +32 -0
  131. data/proto/envoy/config/endpoint/v3/BUILD +16 -0
  132. data/proto/envoy/config/endpoint/v3/endpoint.proto +137 -0
  133. data/proto/envoy/config/endpoint/v3/endpoint_components.proto +229 -0
  134. data/proto/envoy/config/endpoint/v3/load_report.proto +220 -0
  135. data/proto/envoy/config/listener/v3/BUILD +18 -0
  136. data/proto/envoy/config/listener/v3/api_listener.proto +34 -0
  137. data/proto/envoy/config/listener/v3/listener.proto +455 -0
  138. data/proto/envoy/config/listener/v3/listener_components.proto +353 -0
  139. data/proto/envoy/config/listener/v3/quic_config.proto +108 -0
  140. data/proto/envoy/config/listener/v3/udp_listener_config.proto +52 -0
  141. data/proto/envoy/config/route/v3/BUILD +19 -0
  142. data/proto/envoy/config/route/v3/route.proto +172 -0
  143. data/proto/envoy/config/route/v3/route_components.proto +2918 -0
  144. data/proto/envoy/config/route/v3/scoped_route.proto +133 -0
  145. data/proto/envoy/extensions/transport_sockets/tls/v3/BUILD +14 -0
  146. data/proto/envoy/extensions/transport_sockets/tls/v3/cert.proto +12 -0
  147. data/proto/envoy/extensions/transport_sockets/tls/v3/common.proto +597 -0
  148. data/proto/envoy/extensions/transport_sockets/tls/v3/secret.proto +61 -0
  149. data/proto/envoy/extensions/transport_sockets/tls/v3/tls.proto +366 -0
  150. data/proto/envoy/extensions/transport_sockets/tls/v3/tls_spiffe_validator_config.proto +67 -0
  151. data/proto/envoy/service/README.md +3 -0
  152. data/proto/envoy/service/discovery/v3/BUILD +13 -0
  153. data/proto/envoy/service/discovery/v3/ads.proto +44 -0
  154. data/proto/envoy/service/discovery/v3/discovery.proto +443 -0
  155. data/proto/envoy/type/BUILD +9 -0
  156. data/proto/envoy/type/hash_policy.proto +28 -0
  157. data/proto/envoy/type/http.proto +24 -0
  158. data/proto/envoy/type/http_status.proto +140 -0
  159. data/proto/envoy/type/matcher/v3/address.proto +22 -0
  160. data/proto/envoy/type/matcher/v3/filter_state.proto +33 -0
  161. data/proto/envoy/type/matcher/v3/http_inputs.proto +71 -0
  162. data/proto/envoy/type/matcher/v3/metadata.proto +110 -0
  163. data/proto/envoy/type/matcher/v3/node.proto +29 -0
  164. data/proto/envoy/type/matcher/v3/number.proto +33 -0
  165. data/proto/envoy/type/matcher/v3/path.proto +31 -0
  166. data/proto/envoy/type/matcher/v3/regex.proto +97 -0
  167. data/proto/envoy/type/matcher/v3/status_code_input.proto +23 -0
  168. data/proto/envoy/type/matcher/v3/string.proto +94 -0
  169. data/proto/envoy/type/matcher/v3/struct.proto +91 -0
  170. data/proto/envoy/type/matcher/v3/value.proto +80 -0
  171. data/proto/envoy/type/metadata/v3/metadata.proto +117 -0
  172. data/proto/envoy/type/percent.proto +52 -0
  173. data/proto/envoy/type/range.proto +43 -0
  174. data/proto/envoy/type/semantic_version.proto +24 -0
  175. data/proto/envoy/type/token_bucket.proto +36 -0
  176. data/proto/envoy/type/v3/BUILD +12 -0
  177. data/proto/envoy/type/v3/hash_policy.proto +43 -0
  178. data/proto/envoy/type/v3/http.proto +24 -0
  179. data/proto/envoy/type/v3/http_status.proto +199 -0
  180. data/proto/envoy/type/v3/percent.proto +57 -0
  181. data/proto/envoy/type/v3/range.proto +50 -0
  182. data/proto/envoy/type/v3/ratelimit_strategy.proto +79 -0
  183. data/proto/envoy/type/v3/ratelimit_unit.proto +37 -0
  184. data/proto/envoy/type/v3/semantic_version.proto +27 -0
  185. data/proto/envoy/type/v3/token_bucket.proto +39 -0
  186. data/proto/google/protobuf/any.proto +162 -0
  187. data/proto/google/protobuf/duration.proto +115 -0
  188. data/proto/google/protobuf/empty.proto +51 -0
  189. data/proto/google/protobuf/struct.proto +95 -0
  190. data/proto/google/protobuf/timestamp.proto +145 -0
  191. data/proto/google/protobuf/wrappers.proto +157 -0
  192. data/proto/google/rpc/status.proto +47 -0
  193. data/proto/readme.md +70 -0
  194. data/proto/udpa/annotations/migrate.proto +49 -0
  195. data/proto/udpa/annotations/security.proto +31 -0
  196. data/proto/udpa/annotations/sensitive.proto +14 -0
  197. data/proto/udpa/annotations/status.proto +34 -0
  198. data/proto/udpa/annotations/versioning.proto +17 -0
  199. data/proto/validate/validate.proto +862 -0
  200. data/proto/xds/annotations/v3/migrate.proto +46 -0
  201. data/proto/xds/annotations/v3/security.proto +30 -0
  202. data/proto/xds/annotations/v3/sensitive.proto +16 -0
  203. data/proto/xds/annotations/v3/status.proto +59 -0
  204. data/proto/xds/annotations/v3/versioning.proto +20 -0
  205. data/proto/xds/core/v3/authority.proto +22 -0
  206. data/proto/xds/core/v3/cidr.proto +25 -0
  207. data/proto/xds/core/v3/collection_entry.proto +55 -0
  208. data/proto/xds/core/v3/context_params.proto +23 -0
  209. data/proto/xds/core/v3/extension.proto +26 -0
  210. data/proto/xds/core/v3/resource.proto +29 -0
  211. data/proto/xds/core/v3/resource_locator.proto +118 -0
  212. data/proto/xds/core/v3/resource_name.proto +42 -0
  213. data/proto/xds/type/matcher/v3/cel.proto +37 -0
  214. data/proto/xds/type/matcher/v3/domain.proto +46 -0
  215. data/proto/xds/type/matcher/v3/http_inputs.proto +23 -0
  216. data/proto/xds/type/matcher/v3/ip.proto +53 -0
  217. data/proto/xds/type/matcher/v3/matcher.proto +144 -0
  218. data/proto/xds/type/matcher/v3/range.proto +69 -0
  219. data/proto/xds/type/matcher/v3/regex.proto +46 -0
  220. data/proto/xds/type/matcher/v3/string.proto +71 -0
  221. data/proto/xds/type/v3/cel.proto +77 -0
  222. data/proto/xds/type/v3/range.proto +40 -0
  223. data/proto/xds/type/v3/typed_struct.proto +44 -0
  224. data/readme.md +37 -0
  225. data/releases.md +5 -0
  226. data/xds/Dockerfile.backend +24 -0
  227. data/xds/Dockerfile.control-plane +22 -0
  228. data/xds/backend_server.rb +68 -0
  229. data/xds/docker-compose.yaml +89 -0
  230. data/xds/go.mod +22 -0
  231. data/xds/go.sum +82 -0
  232. data/xds/readme.md +122 -0
  233. data/xds/test/async/grpc/xds/client.rb +294 -0
  234. data/xds/test/async/grpc/xds/control_plane.rb +94 -0
  235. data/xds/test_server.go +355 -0
  236. data/xds/update_protos.sh +123 -0
  237. data.tar.gz.sig +0 -0
  238. metadata +386 -0
  239. metadata.gz.sig +2 -0
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "async/grpc/xds/control_plane"
7
+ require "async/grpc/xds/client"
8
+ require "async/grpc/xds/resource_builder"
9
+ require "async/grpc/xds/server"
10
+ require "async/http/endpoint"
11
+ require "sus/fixtures/async"
12
+ require "socket"
13
+
14
+ describe Async::GRPC::XDS::ControlPlane do
15
+ include Sus::Fixtures::Async::ReactorContext
16
+
17
+ let(:control_plane) {subject.new}
18
+
19
+ it "publishes cluster resources" do
20
+ control_plane.update_cluster("myservice")
21
+
22
+ response = control_plane.response(
23
+ Async::GRPC::XDS::ControlPlane::CLUSTER_TYPE,
24
+ ["myservice"]
25
+ )
26
+
27
+ expect(response.version_info).to be == "1"
28
+ expect(response.resources.size).to be == 1
29
+ expect(response.resources.first.type_url).to be == Async::GRPC::XDS::ControlPlane::CLUSTER_TYPE
30
+ end
31
+
32
+ it "publishes endpoint resources" do
33
+ control_plane.update_endpoints(
34
+ "myservice",
35
+ [
36
+ {address: "127.0.0.1", port: 50051},
37
+ {address: "127.0.0.2", port: 50052, healthy: false}
38
+ ]
39
+ )
40
+
41
+ response = control_plane.response(
42
+ Async::GRPC::XDS::ControlPlane::ENDPOINT_TYPE,
43
+ ["myservice"]
44
+ )
45
+
46
+ expect(response.version_info).to be == "1"
47
+ expect(response.resources.size).to be == 1
48
+ expect(response.resources.first.type_url).to be == Async::GRPC::XDS::ControlPlane::ENDPOINT_TYPE
49
+ end
50
+
51
+ it "increments resource versions" do
52
+ control_plane.update_endpoints("myservice", [{address: "127.0.0.1", port: 50051}])
53
+ control_plane.update_endpoints("myservice", [{address: "127.0.0.2", port: 50052}])
54
+
55
+ expect(control_plane.version(Async::GRPC::XDS::ControlPlane::ENDPOINT_TYPE)).to be == "2"
56
+ end
57
+
58
+ it "serves resources over ADS" do
59
+ control_plane.update_cluster("myservice")
60
+ control_plane.update_endpoints("myservice", [{address: "127.0.0.1", port: 50051}])
61
+
62
+ port = available_port
63
+ endpoint = Async::HTTP::Endpoint.parse(
64
+ "http://127.0.0.1:#{port}",
65
+ protocol: Async::HTTP::Protocol::HTTP2
66
+ )
67
+ server = Async::GRPC::XDS::Server.new(control_plane)
68
+ server_task = Async{server.run(endpoint)}
69
+
70
+ client = Async::GRPC::XDS::Client.new("myservice", bootstrap: {
71
+ xds_servers: [
72
+ {
73
+ server_uri: "127.0.0.1:#{port}",
74
+ channel_creds: [{type: "insecure"}]
75
+ }
76
+ ],
77
+ node: {id: "test-#{Process.pid}", cluster: "test"}
78
+ })
79
+
80
+ endpoints = client.resolve_endpoints
81
+
82
+ expect(endpoints.map(&:authority)).to be == ["127.0.0.1:50051"]
83
+ ensure
84
+ client&.close
85
+ server_task&.stop
86
+ end
87
+
88
+ def available_port
89
+ server = TCPServer.new("127.0.0.1", 0)
90
+ server.addr[1]
91
+ ensure
92
+ server&.close
93
+ end
94
+ end
@@ -0,0 +1,355 @@
1
+ package main
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "flag"
7
+ "fmt"
8
+ "log"
9
+ "net"
10
+ "net/http"
11
+ "strings"
12
+ "sync"
13
+ "sync/atomic"
14
+
15
+ "google.golang.org/grpc"
16
+ "google.golang.org/grpc/credentials/insecure"
17
+
18
+ "github.com/envoyproxy/go-control-plane/pkg/cache/types"
19
+ "github.com/envoyproxy/go-control-plane/pkg/cache/v3"
20
+ "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
21
+ serverv3 "github.com/envoyproxy/go-control-plane/pkg/server/v3"
22
+
23
+ clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
24
+ corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
25
+ endpointv3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
26
+ discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
27
+ )
28
+
29
+ var (
30
+ port = flag.Int("port", 18000, "xDS server port")
31
+ adminPort = flag.Int("admin-port", 18001, "Admin HTTP server port")
32
+ upstream = flag.String("upstream", "backend-1:50051,backend-2:50052,backend-3:50053", "Comma-separated list of upstream endpoints")
33
+ )
34
+
35
+ // Custom hash function that accepts any node ID for testing
36
+ type anyNodeHash struct{}
37
+
38
+ func (h *anyNodeHash) ID(node *corev3.Node) string {
39
+ return "any"
40
+ }
41
+
42
+ func main() {
43
+ flag.Parse()
44
+
45
+ ctx := context.Background()
46
+ version := uint64(1)
47
+
48
+ // Create snapshot with cluster and endpoints
49
+ snapshot, err := createSnapshot(*upstream, fmt.Sprintf("%d", version))
50
+ if err != nil {
51
+ log.Fatalf("Failed to create snapshot: %v", err)
52
+ }
53
+
54
+ // For testing, accept any node ID by using a custom hash that always returns the same key
55
+ // This allows any client to connect and get the same snapshot
56
+ snapshotCache := cache.NewSnapshotCache(false, &anyNodeHash{}, nil)
57
+ if err := snapshotCache.SetSnapshot(ctx, "any", snapshot); err != nil {
58
+ log.Fatalf("Failed to set snapshot: %v", err)
59
+ }
60
+ log.Printf("Set snapshot for any node ID")
61
+
62
+ // Create callbacks for logging
63
+ callbacks := serverv3.CallbackFuncs{
64
+ StreamOpenFunc: func(ctx context.Context, streamID int64, typeURL string) error {
65
+ log.Printf("Stream opened: streamID=%d, typeURL=%s", streamID, typeURL)
66
+ return nil
67
+ },
68
+ StreamRequestFunc: func(streamID int64, request *discovery.DiscoveryRequest) error {
69
+ log.Printf("Stream request: streamID=%d, typeURL=%s, resource_names=%v", streamID, request.TypeUrl, request.ResourceNames)
70
+ return nil
71
+ },
72
+ }
73
+
74
+ // Create xDS server with callbacks
75
+ srv := serverv3.NewServer(ctx, snapshotCache, callbacks)
76
+
77
+ // Start gRPC server
78
+ baseListener, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
79
+ if err != nil {
80
+ log.Fatalf("Failed to listen: %v", err)
81
+ }
82
+ lis := newTrackingListener(baseListener)
83
+
84
+ // Create gRPC server with insecure credentials (for testing without TLS)
85
+ grpcServer := grpc.NewServer(grpc.Creds(insecure.NewCredentials()))
86
+ discovery.RegisterAggregatedDiscoveryServiceServer(grpcServer, srv)
87
+
88
+ admin := &adminServer{
89
+ ctx: ctx,
90
+ cache: snapshotCache,
91
+ listener: lis,
92
+ version: &version,
93
+ currentConfig: *upstream,
94
+ }
95
+ go admin.serve(*adminPort)
96
+
97
+ log.Printf("xDS test server listening on :%d", *port)
98
+ log.Printf("xDS admin server listening on :%d", *adminPort)
99
+ log.Printf("Serving cluster 'myservice' with endpoints: %s", *upstream)
100
+
101
+ if err := grpcServer.Serve(lis); err != nil {
102
+ log.Fatalf("Failed to serve: %v", err)
103
+ }
104
+ }
105
+
106
+ type adminServer struct {
107
+ ctx context.Context
108
+ cache cache.SnapshotCache
109
+ listener *trackingListener
110
+ version *uint64
111
+ mutex sync.Mutex
112
+ currentConfig string
113
+ }
114
+
115
+ type endpointsRequest struct {
116
+ Upstream string `json:"upstream"`
117
+ Endpoints []string `json:"endpoints"`
118
+ }
119
+
120
+ func (server *adminServer) serve(port int) {
121
+ mux := http.NewServeMux()
122
+ mux.HandleFunc("/endpoints", server.handleEndpoints)
123
+ mux.HandleFunc("/reset-streams", server.handleResetStreams)
124
+ mux.HandleFunc("/status", server.handleStatus)
125
+
126
+ if err := http.ListenAndServe(fmt.Sprintf(":%d", port), mux); err != nil {
127
+ log.Fatalf("Failed to serve admin API: %v", err)
128
+ }
129
+ }
130
+
131
+ func (server *adminServer) handleEndpoints(response http.ResponseWriter, request *http.Request) {
132
+ if request.Method != http.MethodPost {
133
+ http.Error(response, "method not allowed", http.StatusMethodNotAllowed)
134
+ return
135
+ }
136
+
137
+ var body endpointsRequest
138
+ if err := json.NewDecoder(request.Body).Decode(&body); err != nil {
139
+ http.Error(response, err.Error(), http.StatusBadRequest)
140
+ return
141
+ }
142
+
143
+ upstreams := body.Upstream
144
+ if upstreams == "" {
145
+ upstreams = strings.Join(body.Endpoints, ",")
146
+ }
147
+
148
+ nextVersion := atomic.AddUint64(server.version, 1)
149
+ snapshot, err := createSnapshot(upstreams, fmt.Sprintf("%d", nextVersion))
150
+ if err != nil {
151
+ http.Error(response, err.Error(), http.StatusBadRequest)
152
+ return
153
+ }
154
+ if err := server.cache.SetSnapshot(server.ctx, "any", snapshot); err != nil {
155
+ http.Error(response, err.Error(), http.StatusInternalServerError)
156
+ return
157
+ }
158
+
159
+ server.mutex.Lock()
160
+ server.currentConfig = upstreams
161
+ server.mutex.Unlock()
162
+
163
+ log.Printf("Updated snapshot version %d with endpoints: %s", nextVersion, upstreams)
164
+ writeJSON(response, map[string]any{
165
+ "version": nextVersion,
166
+ "endpoints": parseEndpoints(upstreams),
167
+ })
168
+ }
169
+
170
+ func (server *adminServer) handleResetStreams(response http.ResponseWriter, request *http.Request) {
171
+ if request.Method != http.MethodPost {
172
+ http.Error(response, "method not allowed", http.StatusMethodNotAllowed)
173
+ return
174
+ }
175
+
176
+ closed := server.listener.closeConnections()
177
+ log.Printf("Reset %d active xDS connections", closed)
178
+ writeJSON(response, map[string]any{"closed": closed})
179
+ }
180
+
181
+ func (server *adminServer) handleStatus(response http.ResponseWriter, request *http.Request) {
182
+ server.mutex.Lock()
183
+ currentConfig := server.currentConfig
184
+ server.mutex.Unlock()
185
+
186
+ writeJSON(response, map[string]any{
187
+ "version": atomic.LoadUint64(server.version),
188
+ "endpoints": parseEndpoints(currentConfig),
189
+ })
190
+ }
191
+
192
+ func writeJSON(response http.ResponseWriter, value any) {
193
+ response.Header().Set("Content-Type", "application/json")
194
+ if err := json.NewEncoder(response).Encode(value); err != nil {
195
+ log.Printf("Failed to write JSON response: %v", err)
196
+ }
197
+ }
198
+
199
+ type trackingListener struct {
200
+ net.Listener
201
+ mutex sync.Mutex
202
+ connections map[net.Conn]struct{}
203
+ }
204
+
205
+ func newTrackingListener(listener net.Listener) *trackingListener {
206
+ return &trackingListener{
207
+ Listener: listener,
208
+ connections: make(map[net.Conn]struct{}),
209
+ }
210
+ }
211
+
212
+ func (listener *trackingListener) Accept() (net.Conn, error) {
213
+ connection, err := listener.Listener.Accept()
214
+ if err != nil {
215
+ return nil, err
216
+ }
217
+
218
+ tracked := &trackedConn{Conn: connection, listener: listener}
219
+ listener.mutex.Lock()
220
+ listener.connections[tracked] = struct{}{}
221
+ listener.mutex.Unlock()
222
+
223
+ return tracked, nil
224
+ }
225
+
226
+ func (listener *trackingListener) closeConnections() int {
227
+ listener.mutex.Lock()
228
+ connections := make([]net.Conn, 0, len(listener.connections))
229
+ for connection := range listener.connections {
230
+ connections = append(connections, connection)
231
+ }
232
+ listener.mutex.Unlock()
233
+
234
+ for _, connection := range connections {
235
+ _ = connection.Close()
236
+ }
237
+
238
+ return len(connections)
239
+ }
240
+
241
+ func (listener *trackingListener) remove(connection net.Conn) {
242
+ listener.mutex.Lock()
243
+ delete(listener.connections, connection)
244
+ listener.mutex.Unlock()
245
+ }
246
+
247
+ type trackedConn struct {
248
+ net.Conn
249
+ listener *trackingListener
250
+ once sync.Once
251
+ }
252
+
253
+ func (connection *trackedConn) Close() error {
254
+ err := connection.Conn.Close()
255
+ connection.once.Do(func() {
256
+ connection.listener.remove(connection)
257
+ })
258
+ return err
259
+ }
260
+
261
+ func createSnapshot(upstreams string, version string) (*cache.Snapshot, error) {
262
+ // Parse upstream endpoints
263
+ endpoints := parseEndpoints(upstreams)
264
+
265
+ // Create cluster
266
+ cluster := &clusterv3.Cluster{
267
+ Name: "myservice",
268
+ ClusterDiscoveryType: &clusterv3.Cluster_Type{Type: clusterv3.Cluster_EDS},
269
+ LbPolicy: clusterv3.Cluster_ROUND_ROBIN,
270
+ EdsClusterConfig: &clusterv3.Cluster_EdsClusterConfig{
271
+ ServiceName: "myservice",
272
+ EdsConfig: &corev3.ConfigSource{
273
+ ConfigSourceSpecifier: &corev3.ConfigSource_Ads{},
274
+ },
275
+ },
276
+ }
277
+
278
+ // Create endpoint assignment
279
+ lbEndpoints := make([]*endpointv3.LbEndpoint, 0, len(endpoints))
280
+ for _, ep := range endpoints {
281
+ lbEndpoints = append(lbEndpoints, &endpointv3.LbEndpoint{
282
+ HostIdentifier: &endpointv3.LbEndpoint_Endpoint{
283
+ Endpoint: &endpointv3.Endpoint{
284
+ Address: &corev3.Address{
285
+ Address: &corev3.Address_SocketAddress{
286
+ SocketAddress: &corev3.SocketAddress{
287
+ Protocol: corev3.SocketAddress_TCP,
288
+ Address: ep.Host,
289
+ PortSpecifier: &corev3.SocketAddress_PortValue{
290
+ PortValue: ep.Port,
291
+ },
292
+ },
293
+ },
294
+ },
295
+ },
296
+ },
297
+ HealthStatus: corev3.HealthStatus_HEALTHY,
298
+ })
299
+ }
300
+
301
+ endpointAssignment := &endpointv3.ClusterLoadAssignment{
302
+ ClusterName: "myservice",
303
+ Endpoints: []*endpointv3.LocalityLbEndpoints{
304
+ {
305
+ LbEndpoints: lbEndpoints,
306
+ },
307
+ },
308
+ }
309
+
310
+ // Create snapshot
311
+ // types.Resource is proto.Message, which Cluster and ClusterLoadAssignment implement
312
+ return cache.NewSnapshot(
313
+ version,
314
+ map[resource.Type][]types.Resource{
315
+ resource.ClusterType: {cluster},
316
+ resource.EndpointType: {endpointAssignment},
317
+ },
318
+ )
319
+ }
320
+
321
+ type endpoint struct {
322
+ Host string `json:"host"`
323
+ Port uint32 `json:"port"`
324
+ }
325
+
326
+ func parseEndpoints(upstreams string) []endpoint {
327
+ var endpoints []endpoint
328
+ parts := splitComma(upstreams)
329
+ for _, part := range parts {
330
+ part = strings.TrimSpace(part)
331
+ if part == "" {
332
+ continue
333
+ }
334
+
335
+ host, port := parseHostPort(part)
336
+ endpoints = append(endpoints, endpoint{Host: host, Port: port})
337
+ }
338
+ return endpoints
339
+ }
340
+
341
+ func splitComma(s string) []string {
342
+ return strings.Split(s, ",")
343
+ }
344
+
345
+ func parseHostPort(addr string) (string, uint32) {
346
+ parts := strings.Split(addr, ":")
347
+ if len(parts) == 2 {
348
+ var port uint32
349
+ fmt.Sscanf(parts[1], "%d", &port)
350
+ if port > 0 {
351
+ return parts[0], port
352
+ }
353
+ }
354
+ return addr, 50051
355
+ }
@@ -0,0 +1,123 @@
1
+ #!/bin/bash
2
+ # Update Envoy protobuf definitions
3
+ # This script clones the envoy data-plane-api and copies only the needed .proto files
4
+
5
+ set -e
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
9
+ PROTO_DIR="$PROJECT_ROOT/proto"
10
+ TEMP_DIR="/tmp/envoy-api-$$"
11
+
12
+ echo "Cloning envoyproxy/data-plane-api..."
13
+
14
+ # Clone with sparse checkout to only get what we need
15
+ git clone --depth 1 --filter=blob:none --sparse https://github.com/envoyproxy/data-plane-api.git "$TEMP_DIR"
16
+
17
+ cd "$TEMP_DIR"
18
+
19
+ echo "Setting up sparse checkout..."
20
+
21
+ # First get envoy config files
22
+ git sparse-checkout set \
23
+ envoy/config/cluster/v3 \
24
+ envoy/config/endpoint/v3 \
25
+ envoy/config/listener/v3 \
26
+ envoy/config/route/v3 \
27
+ envoy/config/core/v3 \
28
+ envoy/extensions/transport_sockets/tls/v3
29
+
30
+ # Copy envoy config files
31
+ cp -r envoy "$PROTO_DIR/"
32
+
33
+ # Now get discovery service and google protobuf files
34
+ git sparse-checkout set \
35
+ envoy/service/discovery/v3 \
36
+ google/protobuf
37
+
38
+ # Copy discovery service
39
+ cp -r envoy/service "$PROTO_DIR/envoy/"
40
+
41
+ # Copy google protobuf (if exists in repo)
42
+ if [ -d "google" ]; then
43
+ cp -r google "$PROTO_DIR/"
44
+ fi
45
+
46
+ # Get Google protobuf well-known types from protobuf repo
47
+ echo "Fetching Google protobuf well-known types..."
48
+ git clone --depth 1 https://github.com/protocolbuffers/protobuf.git /tmp/protobuf-$$
49
+ mkdir -p "$PROTO_DIR/google/protobuf"
50
+ cp /tmp/protobuf-$$/src/google/protobuf/{any,duration,timestamp,struct,empty,wrappers}.proto "$PROTO_DIR/google/protobuf/" 2>/dev/null || true
51
+ rm -rf /tmp/protobuf-$$
52
+
53
+ # Get google/rpc/status.proto from api-common-protos
54
+ echo "Fetching google/rpc/status.proto..."
55
+ git clone --depth 1 https://github.com/googleapis/api-common-protos.git /tmp/api-common-$$
56
+ mkdir -p "$PROTO_DIR/google/rpc"
57
+ cp /tmp/api-common-$$/google/rpc/status.proto "$PROTO_DIR/google/rpc/" 2>/dev/null || true
58
+ rm -rf /tmp/api-common-$$
59
+
60
+ # Get envoy/type/v3 and xds/core/v3 from data-plane-api
61
+ echo "Fetching envoy/type/v3 and xds/core/v3..."
62
+ git clone --depth 1 --filter=blob:none --sparse https://github.com/envoyproxy/data-plane-api.git /tmp/envoy-types-$$
63
+ cd /tmp/envoy-types-$$
64
+ git sparse-checkout set envoy/type/v3 envoy/type/matcher/v3 envoy/type/metadata/v3 xds/core/v3
65
+ mkdir -p "$PROTO_DIR/envoy/type/v3" "$PROTO_DIR/envoy/type/matcher/v3" "$PROTO_DIR/envoy/type/metadata/v3" "$PROTO_DIR/xds/core/v3"
66
+ cp -r envoy/type "$PROTO_DIR/envoy/" 2>/dev/null || true
67
+ cp -r xds/core "$PROTO_DIR/xds/" 2>/dev/null || true
68
+ rm -rf /tmp/envoy-types-$$
69
+
70
+ # Get xds/type/matcher/v3 and xds/core/v3 from cncf/xds repo
71
+ echo "Fetching xds/type/matcher/v3 and xds/core/v3..."
72
+ git clone --depth 1 https://github.com/cncf/xds.git /tmp/xds-types-$$
73
+ mkdir -p "$PROTO_DIR/xds/type/matcher/v3" "$PROTO_DIR/xds/core/v3"
74
+ find /tmp/xds-types-$$/xds/type/matcher/v3 -name "*.proto" ! -name "*cel*" -exec cp {} "$PROTO_DIR/xds/type/matcher/v3/" \;
75
+ find /tmp/xds-types-$$/xds/core/v3 -name "*.proto" -exec cp {} "$PROTO_DIR/xds/core/v3/" \;
76
+ rm -rf /tmp/xds-types-$$
77
+
78
+ # Get udpa annotations
79
+ echo "Fetching udpa annotations..."
80
+ git clone --depth 1 https://github.com/cncf/udpa.git /tmp/udpa-$$
81
+ mkdir -p "$PROTO_DIR/udpa/annotations"
82
+ cp /tmp/udpa-$$/udpa/annotations/*.proto "$PROTO_DIR/udpa/annotations/" 2>/dev/null || true
83
+ rm -rf /tmp/udpa-$$
84
+
85
+ # Get validate annotations
86
+ echo "Fetching validate annotations..."
87
+ git clone --depth 1 https://github.com/envoyproxy/protoc-gen-validate.git /tmp/validate-$$
88
+ mkdir -p "$PROTO_DIR/validate"
89
+ cp /tmp/validate-$$/validate/validate.proto "$PROTO_DIR/validate/" 2>/dev/null || true
90
+ rm -rf /tmp/validate-$$
91
+
92
+ # Get envoy annotations
93
+ echo "Fetching envoy annotations..."
94
+ git clone --depth 1 --filter=blob:none --sparse https://github.com/envoyproxy/envoy.git /tmp/envoy-annotations-$$
95
+ cd /tmp/envoy-annotations-$$
96
+ git sparse-checkout set api/envoy/annotations
97
+ mkdir -p "$PROTO_DIR/envoy/annotations"
98
+ cp api/envoy/annotations/*.proto "$PROTO_DIR/envoy/annotations/" 2>/dev/null || true
99
+ rm -rf /tmp/envoy-annotations-$$
100
+
101
+ echo "Copying .proto files to $PROTO_DIR..."
102
+
103
+ # Create directories
104
+ mkdir -p "$PROTO_DIR/envoy/service/discovery/v3"
105
+ mkdir -p "$PROTO_DIR/envoy/config/cluster/v3"
106
+ mkdir -p "$PROTO_DIR/envoy/config/endpoint/v3"
107
+ mkdir -p "$PROTO_DIR/envoy/config/listener/v3"
108
+ mkdir -p "$PROTO_DIR/envoy/config/route/v3"
109
+ mkdir -p "$PROTO_DIR/envoy/config/core/v3"
110
+ mkdir -p "$PROTO_DIR/envoy/extensions/transport_sockets/tls/v3"
111
+ mkdir -p "$PROTO_DIR/google/protobuf"
112
+
113
+ # Copy files
114
+ cp -r envoy "$PROTO_DIR/"
115
+ cp -r google "$PROTO_DIR/"
116
+
117
+ # Cleanup
118
+ rm -rf "$TEMP_DIR"
119
+
120
+ echo "Done! Proto files updated in $PROTO_DIR"
121
+ echo ""
122
+ echo "To generate Ruby code, run:"
123
+ echo " bundle exec bake async:grpc:xds:generate_protos"
data.tar.gz.sig ADDED
Binary file