コード例 #1
0
        public async Task TestAggregateHandlerDefaultRequestType()
        {
            var configWatcher = new MockConfigWatcher(true, new Dictionary <string, (string, IEnumerable <IMessage>)>());

            using var server = DiscoveryServerBuilder
                               .CreateFor(configWatcher)
                               .ConfigureDiscoveryService <AggregatedDiscoveryService>()
                               .Build();
            await server.StartAsync();

            var client = new AggregatedDiscoveryServiceClient(CreateGrpcChannel());

            var duplex     = client.StreamAggregatedResources();
            var clientTask = HandleResponses(duplex.ResponseStream);

            // Leave off the type URL. For ADS requests it should fail because the type URL is required.
            await duplex.RequestStream.WriteAsync(new DiscoveryRequest
            {
                Node = NODE,
            });

            await duplex.RequestStream.CompleteAsync();

            var(responseErrors, responses, completed, error) = await clientTask;
        }
コード例 #2
0
        public async Task TestCallbacksOnError()
        {
            int streamClosesWithErrors = 0;
            var callbacks = Substitute.For <IDiscoveryServerCallbacks>();

            callbacks
            .When(x => x.OnStreamCloseWithError(Arg.Any <long>(), Arg.Any <string>(), Arg.Any <Exception>()))
            .Do(_ => Interlocked.Increment(ref streamClosesWithErrors));

            var configWatcher = new MockConfigWatcher(false, CreateResponses());

            using var server = DiscoveryServerBuilder
                               .CreateFor(configWatcher, callbacks)
                               .ConfigureDiscoveryService <AggregatedDiscoveryService>()
                               .Build();

            await server.StartAsync();

            var client = new AggregatedDiscoveryServiceClient(CreateGrpcChannel());
            var ctx    = new CancellationTokenSource();
            var duplex = client.StreamAggregatedResources(cancellationToken: ctx.Token);
            await duplex.RequestStream.WriteAsync(new DiscoveryRequest());

            ctx.Cancel();
            WaitUntil(ref streamClosesWithErrors, 1, TimeSpan.FromSeconds(1));
            streamClosesWithErrors.Should().Be(1);
        }
コード例 #3
0
        public async Task TestSendError()
        {
            var configWatcher = new MockConfigWatcher(false, CreateResponses());

            using var server = DiscoveryServerBuilder
                               .CreateFor(configWatcher)
                               .ConfigureDiscoveryService <AggregatedDiscoveryService>()
                               .Build();
            await server.StartAsync();

            var client = new AggregatedDiscoveryServiceClient(CreateGrpcChannel());

            foreach (var typeUrl in Resources.TYPE_URLS)
            {
                var duplex     = client.StreamAggregatedResources();
                var clientTask = HandleResponses(duplex.ResponseStream, sendError: true);


                await duplex.RequestStream.WriteAsync(new DiscoveryRequest
                {
                    Node    = NODE,
                    TypeUrl = typeUrl
                });

                var(responseErrors, responses, completed, error) = await clientTask;
                completed.Should().BeFalse();
                error.Should().BeTrue();
                responseErrors.Should().BeEmpty();
            }
        }
コード例 #4
0
        public async Task TestWatchClosed()
        {
            var configWatcher = new MockConfigWatcher(true, new Dictionary <string, (string, IEnumerable <IMessage>)>());

            using var server = DiscoveryServerBuilder
                               .CreateFor(configWatcher)
                               .ConfigureDiscoveryService <AggregatedDiscoveryService>()
                               .Build();
            await server.StartAsync();

            var client = new AggregatedDiscoveryServiceClient(CreateGrpcChannel());

            foreach (var typeUrl in Resources.TYPE_URLS)
            {
                // MockDiscoveryResponseObserver responseObserver = new MockDiscoveryResponseObserver();
                var ctx        = new CancellationTokenSource();
                var duplex     = client.StreamAggregatedResources(cancellationToken: ctx.Token);
                var clientTask = HandleResponses(duplex.ResponseStream);

                await duplex.RequestStream.WriteAsync(new DiscoveryRequest
                {
                    Node    = NODE,
                    TypeUrl = typeUrl
                });

                ctx.Cancel();
                var(responseErrors, responses, completed, error) = await clientTask;
                error.Should().BeTrue();
                completed.Should().BeFalse();
                responseErrors.Should().BeEmpty();
            }
        }
コード例 #5
0
        public static void Main(string[] args)
        {
            // First create a cache object to hold the envoy configuration cache
            // the cache requires a node hash callback. It'll give you an envoy Node, and expects an Id.
            // this hard codes the id to "key", but it will change later on.
            var cache = new SimpleCache <string>(_ => "key");

            cache.SetSnapshot("key", Data.CreateBaseSnapshot());

            // Create a DiscoveryServer and give it the cache object
            // Select the type of server. https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol
            var discoveryServer = DiscoveryServerBuilder
                                  .CreateFor(cache, 6000)
                                  .UseHttps(false)
                                  .ConfigureDiscoveryService <ClusterDiscoveryService>()
                                  .ConfigureDiscoveryService <EndpointDiscoveryService>()
                                  .ConfigureDiscoveryService <RouteDiscoveryService>()
                                  .ConfigureDiscoveryService <ListenerDiscoveryService>()
                                  .ConfigureLogging(logging =>
            {
                logging.AddConsole();
                logging.SetMinimumLevel(LogLevel.Debug);
            })
                                  .Build();

            var restServer = WebHost.CreateDefaultBuilder(args)
                             .Configure(_ => _.Run(async ctx =>
            {
                var endpointRequest = await deserialize <AppEndpoint>(ctx.Response.Body);
                if (valid(endpointRequest))
                {
                    var result = Reconcile(cache, endpointRequest);
                    await ctx.Response.WriteAsync(result);
                }
                else
                {
                    ctx.Response.StatusCode = 400;
                    await ctx.Response.WriteAsync("Invalid");
                }


                bool valid(AppEndpoint e)
                => !string.IsNullOrEmpty(e.Ip) &&
                !string.IsNullOrEmpty(e.Domain) &&
                !string.IsNullOrEmpty(e.Name) &&
                e.Port != 0;

                async Task <T> deserialize <T>(Stream stream)
                {
                    var str = await new StreamReader(ctx.Request.Body).ReadToEndAsync();
                    return(JsonConvert.DeserializeObject <T>(str));
                }
            }))
                             .Build();

            discoveryServer.RunAsync();
            restServer.Run();
        }
コード例 #6
0
        public async Task TestStaleNonce()
        {
            var configWatcher = new MockConfigWatcher(false, CreateResponses());

            using var server = DiscoveryServerBuilder
                               .CreateFor(configWatcher)
                               .ConfigureDiscoveryService <AggregatedDiscoveryService>()
                               .Build();
            await server.StartAsync();

            var client = new AggregatedDiscoveryServiceClient(CreateGrpcChannel());

            foreach (var typeUrl in Resources.TYPE_URLS)
            {
                var duplex     = client.StreamAggregatedResources();
                var clientTask = HandleResponses(duplex.ResponseStream);

                await duplex.RequestStream.WriteAsync(new DiscoveryRequest
                {
                    Node    = NODE,
                    TypeUrl = typeUrl,
                });

                // Stale request, should not create a new watch.
                await duplex.RequestStream.WriteAsync(new DiscoveryRequest
                {
                    Node          = NODE,
                    TypeUrl       = typeUrl,
                    ResponseNonce = "xyz"
                });

                // Fresh request, should create a new watch.
                await duplex.RequestStream.WriteAsync(new DiscoveryRequest
                {
                    Node          = NODE,
                    TypeUrl       = typeUrl,
                    ResponseNonce = "1",
                    VersionInfo   = "0"
                });

                await duplex.RequestStream.CompleteAsync();

                var(responseErrors, responses, completed, error) = await clientTask;

                // Assert that 2 watches have been created for this resource type.
                configWatcher.counts[typeUrl].Should().Be(2);
            }
        }
コード例 #7
0
        public async Task TestCallbacksSeparateHandlers()
        {
            var streamCloses    = new ConcurrentDictionary <string, StrongBox <int> >();
            var streamOpens     = new ConcurrentDictionary <string, StrongBox <int> >();
            var streamRequests  = new ConcurrentDictionary <string, StrongBox <int> >();
            var streamResponses = new ConcurrentDictionary <string, StrongBox <int> >();

            Resources.TYPE_URLS.ForEach(typeUrl =>
            {
                streamCloses[typeUrl]    = new StrongBox <int>(0);
                streamOpens[typeUrl]     = new StrongBox <int>(0);
                streamRequests[typeUrl]  = new StrongBox <int>(0);
                streamResponses[typeUrl] = new StrongBox <int>(0);
            });

            var assertionErrors = new List <string>();
            var callbacks       = Substitute.For <IDiscoveryServerCallbacks>();

            void ValidateTypeUrl(string typeUrl, string caller)
            {
                if (!Resources.TYPE_URLS.Contains(typeUrl))
                {
                    assertionErrors.Add($"{caller}#typeUrl => expected {Resources.ANY_TYPE_URL}, got {typeUrl}");
                }
            }

            callbacks
            .When(x => x.OnStreamClose(Arg.Any <long>(), Arg.Any <string>()))
            .Do(args =>
            {
                var typeUrl = args.ArgAt <string>(1);
                ValidateTypeUrl(typeUrl, "OnStreamClose");
                Interlocked.Increment(ref streamCloses[typeUrl].Value);
            });

            callbacks
            .When(x => x.OnStreamOpen(Arg.Any <long>(), Arg.Any <string>()))
            .Do(args =>
            {
                var typeUrl = args.ArgAt <string>(1);
                ValidateTypeUrl(typeUrl, "OnStreamOpen");
                Interlocked.Increment(ref streamOpens[typeUrl].Value);
            });

            callbacks
            .When(x => x.OnStreamRequest(Arg.Any <long>(), Arg.Any <DiscoveryRequest>()))
            .Do(args => Interlocked.Increment(ref streamRequests[args.ArgAt <DiscoveryRequest>(1).TypeUrl].Value));

            callbacks
            .When(x => x.OnStreamResponse(Arg.Any <long>(), Arg.Any <DiscoveryRequest>(), Arg.Any <DiscoveryResponse>()))
            .Do(args => Interlocked.Increment(ref streamResponses[args.ArgAt <DiscoveryRequest>(1).TypeUrl].Value));

            var configWatcher = new MockConfigWatcher(false, CreateResponses());

            using var server = DiscoveryServerBuilder
                               .CreateFor(configWatcher, callbacks)
                               .ConfigureDiscoveryService <ClusterDiscoveryService>()
                               .ConfigureDiscoveryService <EndpointDiscoveryService>()
                               .ConfigureDiscoveryService <ListenerDiscoveryService>()
                               .ConfigureDiscoveryService <RouteDiscoveryService>()
                               .ConfigureDiscoveryService <SecretDiscoveryService>()
                               .Build();
            await server.StartAsync();

            var channel        = CreateGrpcChannel();
            var clusterClient  = new ClusterDiscoveryServiceClient(channel);
            var endpointClient = new EndpointDiscoveryServiceClient(channel);
            var listenerClient = new ListenerDiscoveryServiceClient(channel);
            var routeClient    = new RouteDiscoveryServiceClient(channel);
            var secretClient   = new SecretDiscoveryServiceClient(channel);

            foreach (var typeUrl in Resources.TYPE_URLS)
            {
                Task <(List <string>, List <DiscoveryResponse>, bool, bool)>   clientTask = null;
                AsyncDuplexStreamingCall <DiscoveryRequest, DiscoveryResponse> duplex     = null;
                switch (typeUrl)
                {
                case Resources.CLUSTER_TYPE_URL:
                    duplex     = clusterClient.StreamClusters();
                    clientTask = Task.Run(async() => await HandleResponses(duplex.ResponseStream));
                    break;

                case Resources.ENDPOINT_TYPE_URL:
                    duplex     = endpointClient.StreamEndpoints();
                    clientTask = Task.Run(async() => await HandleResponses(duplex.ResponseStream));
                    break;

                case Resources.LISTENER_TYPE_URL:
                    duplex     = listenerClient.StreamListeners();
                    clientTask = Task.Run(async() => await HandleResponses(duplex.ResponseStream));
                    break;

                case Resources.ROUTE_TYPE_URL:
                    duplex     = routeClient.StreamRoutes();
                    clientTask = Task.Run(async() => await HandleResponses(duplex.ResponseStream));
                    break;

                case Resources.SECRET_TYPE_URL:
                    duplex     = secretClient.StreamSecrets();
                    clientTask = Task.Run(async() => await HandleResponses(duplex.ResponseStream));
                    break;

                default:
                    Assert.True(false, "Unsupported resource type: " + typeUrl);
                    break;
                }

                var discoveryRequest = new DiscoveryRequest
                {
                    Node    = NODE,
                    TypeUrl = typeUrl
                };

                await duplex.RequestStream.WriteAsync(discoveryRequest);

                WaitUntil(ref streamOpens[typeUrl].Value, 1);
                streamOpens[typeUrl].Value.Should().Be(1);

                WaitUntil(ref streamRequests[typeUrl].Value, 1);
                streamRequests[typeUrl].Value.Should().Be(1);

                await duplex.RequestStream.CompleteAsync();

                WaitUntil(ref streamResponses[typeUrl].Value, 1);
                streamResponses[typeUrl].Value.Should().Be(1);

                WaitUntil(ref streamCloses[typeUrl].Value, 1);
                streamCloses[typeUrl].Value.Should().Be(1);
            }

            assertionErrors.Should().BeEmpty();
        }
コード例 #8
0
        public async Task TestAggregatedHandler()
        {
            var configWatcher = new MockConfigWatcher(false, CreateResponses());

            using var server = DiscoveryServerBuilder
                               .CreateFor(configWatcher)
                               .ConfigureDiscoveryService <AggregatedDiscoveryService>()
                               .Build();

            await server.StartAsync();

            var client = new AggregatedDiscoveryServiceClient(CreateGrpcChannel());

            var duplex     = client.StreamAggregatedResources();
            var clientTask = Task.Run(async() => await HandleResponses(duplex.ResponseStream));

            await duplex.RequestStream.WriteAsync(new DiscoveryRequest
            {
                Node    = NODE,
                TypeUrl = Resources.LISTENER_TYPE_URL
            });

            await duplex.RequestStream.WriteAsync(new DiscoveryRequest
            {
                Node    = NODE,
                TypeUrl = Resources.CLUSTER_TYPE_URL,
            });

            var clusterRequest = new DiscoveryRequest
            {
                Node    = NODE,
                TypeUrl = Resources.ENDPOINT_TYPE_URL,
            };

            clusterRequest.ResourceNames.Add(CLUSTER_NAME);
            await duplex.RequestStream.WriteAsync(clusterRequest);

            var routeRequest = new DiscoveryRequest
            {
                Node    = NODE,
                TypeUrl = Resources.ROUTE_TYPE_URL,
            };

            routeRequest.ResourceNames.Add(ROUTE_NAME);
            await duplex.RequestStream.WriteAsync(routeRequest);

            var secretRequest = new DiscoveryRequest
            {
                Node    = NODE,
                TypeUrl = Resources.SECRET_TYPE_URL,
            };

            secretRequest.ResourceNames.Add(SECRET_NAME);
            await duplex.RequestStream.WriteAsync(secretRequest);

            await duplex.RequestStream.CompleteAsync();

            var(responseErrors, responses, completed, error) = await clientTask;
            completed.Should().BeTrue();
            error.Should().BeFalse();
            responseErrors.Should().BeEmpty();

            foreach (var typeUrl in Resources.TYPE_URLS)
            {
                configWatcher.counts.Should().Contain(typeUrl, 1);
            }

            configWatcher.counts.Should().HaveCount(Resources.TYPE_URLS.Count());

            foreach (var typeUrl in Resources.TYPE_URLS)
            {
                responses.Should().Contain(r => r.TypeUrl == typeUrl && r.VersionInfo == VERSION,
                                           "missing expected response of type %s", typeUrl);
            }
        }
コード例 #9
0
        public async Task TestCallbacksAggregateHandler()
        {
            var assertionErrors = new List <string>();
            int streamCloses = 0, streamOpens = 0, streamRequests = 0, streamResponses = 0;
            var callbacks = Substitute.For <IDiscoveryServerCallbacks>();

            void ValidateTypeUrl(string typeUrl, string caller)
            {
                if (typeUrl != Resources.ANY_TYPE_URL)
                {
                    assertionErrors.Add($"{caller}#typeUrl => expected {Resources.ANY_TYPE_URL}, got {typeUrl}");
                }
            }

            callbacks
            .When(x => x.OnStreamClose(Arg.Any <long>(), Arg.Any <string>()))
            .Do(args =>
            {
                ValidateTypeUrl(args.ArgAt <string>(1), "OnStreamClose");
                Interlocked.Increment(ref streamCloses);
            });

            callbacks
            .When(x => x.OnStreamOpen(Arg.Any <long>(), Arg.Any <string>()))
            .Do(args =>
            {
                ValidateTypeUrl(args.ArgAt <string>(1), "OnStreamOpen");
                Interlocked.Increment(ref streamOpens);
            });

            callbacks
            .When(x => x.OnStreamRequest(Arg.Any <long>(), Arg.Any <DiscoveryRequest>()))
            .Do(_ => Interlocked.Increment(ref streamRequests));

            callbacks
            .When(x => x.OnStreamResponse(Arg.Any <long>(), Arg.Any <DiscoveryRequest>(), Arg.Any <DiscoveryResponse>()))
            .Do(_ => Interlocked.Increment(ref streamResponses));


            var configWatcher = new MockConfigWatcher(false, CreateResponses());

            using var server = DiscoveryServerBuilder
                               .CreateFor(configWatcher, callbacks)
                               .ConfigureDiscoveryService <AggregatedDiscoveryService>()
                               .Build();
            await server.StartAsync();

            var client     = new AggregatedDiscoveryServiceClient(CreateGrpcChannel());
            var duplex     = client.StreamAggregatedResources();
            var clientTask = HandleResponses(duplex.ResponseStream);

            await duplex.RequestStream.WriteAsync(new DiscoveryRequest
            {
                Node    = NODE,
                TypeUrl = Resources.LISTENER_TYPE_URL
            });

            WaitUntil(ref streamOpens, 1);
            streamOpens.Should().Be(1);

            await duplex.RequestStream.WriteAsync(new DiscoveryRequest
            {
                Node    = NODE,
                TypeUrl = Resources.CLUSTER_TYPE_URL
            });

            var cluster = new DiscoveryRequest
            {
                Node    = NODE,
                TypeUrl = Resources.ENDPOINT_TYPE_URL,
            };

            cluster.ResourceNames.Add(CLUSTER_NAME);

            await duplex.RequestStream.WriteAsync(cluster);

            var route = new DiscoveryRequest
            {
                Node    = NODE,
                TypeUrl = Resources.ROUTE_TYPE_URL
            };

            route.ResourceNames.Add(ROUTE_NAME);
            await duplex.RequestStream.WriteAsync(route);

            var secret = new DiscoveryRequest
            {
                Node    = NODE,
                TypeUrl = Resources.SECRET_TYPE_URL,
            };

            secret.ResourceNames.Add(SECRET_NAME);
            await duplex.RequestStream.WriteAsync(secret);


            WaitUntil(ref streamRequests, Resources.TYPE_URLS.Count());
            streamRequests.Should().Be(Resources.TYPE_URLS.Count());

            WaitUntil(ref streamRequests, Resources.TYPE_URLS.Count());
            streamResponses.Should().Be(Resources.TYPE_URLS.Count());

            // Send another round of requests. These should not trigger any responses.
            streamResponses = 0;
            streamRequests  = 0;

            await duplex.RequestStream.WriteAsync(new DiscoveryRequest
            {
                Node          = NODE,
                ResponseNonce = "0",
                VersionInfo   = VERSION,
                TypeUrl       = Resources.LISTENER_TYPE_URL
            });

            await duplex.RequestStream.WriteAsync(new DiscoveryRequest
            {
                Node          = NODE,
                ResponseNonce = "1",
                TypeUrl       = Resources.CLUSTER_TYPE_URL,
                VersionInfo   = VERSION,
            });

            var cluster2 = new DiscoveryRequest
            {
                Node          = NODE,
                ResponseNonce = "2",
                TypeUrl       = Resources.ENDPOINT_TYPE_URL,
                VersionInfo   = VERSION
            };

            cluster2.ResourceNames.Add(CLUSTER_NAME);
            await duplex.RequestStream.WriteAsync(cluster2);

            var route2 = new DiscoveryRequest
            {
                Node          = NODE,
                ResponseNonce = "3",
                TypeUrl       = Resources.ROUTE_TYPE_URL,
                VersionInfo   = VERSION
            };

            route2.ResourceNames.Add(ROUTE_NAME);
            await duplex.RequestStream.WriteAsync(route2);

            var secert2 = new DiscoveryRequest
            {
                Node          = NODE,
                ResponseNonce = "4",
                TypeUrl       = Resources.SECRET_TYPE_URL,
                VersionInfo   = VERSION,
            };

            secert2.ResourceNames.Add(SECRET_NAME);
            await duplex.RequestStream.WriteAsync(secert2);

            WaitUntil(ref streamRequests, Resources.TYPE_URLS.Count());
            streamRequests.Should().Be(Resources.TYPE_URLS.Count());
            streamResponses.Should().Be(0);

            await duplex.RequestStream.CompleteAsync();

            WaitUntil(ref streamCloses, 1);
            streamCloses.Should().Be(1);

            assertionErrors.Should().BeEmpty();
        }
コード例 #10
0
        public async Task TestSeparateHandlersDefaultRequestType()
        {
            var configWatcher = new MockConfigWatcher(false, CreateResponses());

            using var server = DiscoveryServerBuilder
                               .CreateFor(configWatcher)
                               .ConfigureDiscoveryService <ClusterDiscoveryService>()
                               .ConfigureDiscoveryService <EndpointDiscoveryService>()
                               .ConfigureDiscoveryService <ListenerDiscoveryService>()
                               .ConfigureDiscoveryService <RouteDiscoveryService>()
                               .ConfigureDiscoveryService <SecretDiscoveryService>()
                               .Build();
            await server.StartAsync();

            var channel        = CreateGrpcChannel();
            var clusterClient  = new ClusterDiscoveryServiceClient(channel);
            var endpointClient = new EndpointDiscoveryServiceClient(channel);
            var listenerClient = new ListenerDiscoveryServiceClient(channel);
            var routeClient    = new RouteDiscoveryServiceClient(channel);
            var secretClient   = new SecretDiscoveryServiceClient(channel);

            foreach (var typeUrl in Resources.TYPE_URLS)
            {
                Task <(List <string>, List <DiscoveryResponse>, bool, bool)>   clientTask = null;
                AsyncDuplexStreamingCall <DiscoveryRequest, DiscoveryResponse> duplex     = null;
                switch (typeUrl)
                {
                case Resources.CLUSTER_TYPE_URL:
                    duplex     = clusterClient.StreamClusters();
                    clientTask = Task.Run(async() => await HandleResponses(duplex.ResponseStream));
                    break;

                case Resources.ENDPOINT_TYPE_URL:
                    duplex     = endpointClient.StreamEndpoints();
                    clientTask = Task.Run(async() => await HandleResponses(duplex.ResponseStream));
                    break;

                case Resources.LISTENER_TYPE_URL:
                    duplex     = listenerClient.StreamListeners();
                    clientTask = Task.Run(async() => await HandleResponses(duplex.ResponseStream));
                    break;

                case Resources.ROUTE_TYPE_URL:
                    duplex     = routeClient.StreamRoutes();
                    clientTask = Task.Run(async() => await HandleResponses(duplex.ResponseStream));
                    break;

                case Resources.SECRET_TYPE_URL:
                    duplex     = secretClient.StreamSecrets();
                    clientTask = Task.Run(async() => await HandleResponses(duplex.ResponseStream));
                    break;

                default:
                    Assert.True(false, "Unsupported resource type: " + typeUrl);
                    break;
                }

                // Leave off the type URL. For xDS requests it should default to the value for that handler's type.
                var discoveryRequest = new DiscoveryRequest
                {
                    Node = NODE,
                };

                await duplex.RequestStream.WriteAsync(discoveryRequest);

                await duplex.RequestStream.CompleteAsync();

                var(responseErrors, _, _, _) = await clientTask;

                responseErrors.Should().BeEmpty();
            }
        }
コード例 #11
0
        public async Task TestSeparateHandlers()
        {
            var configWatcher = new MockConfigWatcher(false, CreateResponses());

            using var server = DiscoveryServerBuilder
                               .CreateFor(configWatcher)
                               .ConfigureDiscoveryService <ClusterDiscoveryService>()
                               .ConfigureDiscoveryService <EndpointDiscoveryService>()
                               .ConfigureDiscoveryService <ListenerDiscoveryService>()
                               .ConfigureDiscoveryService <RouteDiscoveryService>()
                               .ConfigureDiscoveryService <SecretDiscoveryService>()
                               .Build();
            await server.StartAsync();

            var channel        = CreateGrpcChannel();
            var clusterClient  = new ClusterDiscoveryServiceClient(channel);
            var endpointClient = new EndpointDiscoveryServiceClient(channel);
            var listenerClient = new ListenerDiscoveryServiceClient(channel);
            var routeClient    = new RouteDiscoveryServiceClient(channel);
            var secretClient   = new SecretDiscoveryServiceClient(channel);

            foreach (var typeUrl in Resources.TYPE_URLS)
            {
                var discoveryRequest = new DiscoveryRequest
                {
                    Node    = NODE,
                    TypeUrl = typeUrl
                };
                Task <(List <string>, List <DiscoveryResponse>, bool, bool)>   clientTask = null;
                AsyncDuplexStreamingCall <DiscoveryRequest, DiscoveryResponse> duplex     = null;
                switch (typeUrl)
                {
                case Resources.CLUSTER_TYPE_URL:
                    duplex     = clusterClient.StreamClusters();
                    clientTask = Task.Run(async() => await HandleResponses(duplex.ResponseStream));
                    break;

                case Resources.ENDPOINT_TYPE_URL:
                    duplex     = endpointClient.StreamEndpoints();
                    clientTask = Task.Run(async() => await HandleResponses(duplex.ResponseStream));
                    discoveryRequest.ResourceNames.Add(CLUSTER_NAME);
                    break;

                case Resources.LISTENER_TYPE_URL:
                    duplex     = listenerClient.StreamListeners();
                    clientTask = Task.Run(async() => await HandleResponses(duplex.ResponseStream));
                    break;

                case Resources.ROUTE_TYPE_URL:
                    duplex     = routeClient.StreamRoutes();
                    clientTask = Task.Run(async() => await HandleResponses(duplex.ResponseStream));
                    discoveryRequest.ResourceNames.Add(ROUTE_NAME);
                    break;

                case Resources.SECRET_TYPE_URL:
                    duplex     = secretClient.StreamSecrets();
                    clientTask = Task.Run(async() => await HandleResponses(duplex.ResponseStream));
                    discoveryRequest.ResourceNames.Add(SECRET_NAME);
                    break;

                default:
                    Assert.True(false, "Unsupported resource type: " + typeUrl);
                    break;
                }

                await duplex.RequestStream.WriteAsync(discoveryRequest);

                await duplex.RequestStream.CompleteAsync();

                var(responseErrors, responses, completed, error) = await clientTask;

                completed.Should().BeTrue();
                error.Should().BeFalse();
                responseErrors.Should().BeEmpty();

                configWatcher.counts.Should().Contain(typeUrl, 1);
                responses.Should().Contain(
                    r => r.TypeUrl == typeUrl && r.VersionInfo == VERSION,
                    "missing expected response of type %s", typeUrl);
            }

            configWatcher.counts.Should().HaveCount(Resources.TYPE_URLS.Count());
        }