private async Task <(IList <ProxyRoute>, IList <Exception>)> VerifyRoutesAsync(IReadOnlyList <ProxyRoute> routes, CancellationToken cancellation)
        {
            if (routes == null)
            {
                return(Array.Empty <ProxyRoute>(), Array.Empty <Exception>());
            }

            var seenRouteIds = new HashSet <string>();
            var sortedRoutes = new SortedList <(int, string), ProxyRoute>(routes?.Count ?? 0);
            var errors       = new List <Exception>();

            foreach (var r in routes)
            {
                if (seenRouteIds.Contains(r.RouteId))
                {
                    errors.Add(new ArgumentException($"Duplicate route {r.RouteId}"));
                    continue;
                }

                // Don't modify the original
                var route = r.DeepClone();

                try
                {
                    foreach (var filter in _filters)
                    {
                        await filter.ConfigureRouteAsync(route, cancellation);
                    }
                }
                catch (Exception ex)
                {
                    errors.Add(new Exception($"An exception was thrown from the configuration callbacks for route '{r.RouteId}'.", ex));
                    continue;
                }

                var routeErrors = await _configValidator.ValidateRouteAsync(route);

                if (routeErrors.Count > 0)
                {
                    errors.AddRange(routeErrors);
                    continue;
                }

                sortedRoutes.Add((route.Order ?? 0, route.RouteId), route);
            }

            if (errors.Count > 0)
            {
                return(null, errors);
            }

            return(sortedRoutes.Values, errors);
        }
Beispiel #2
0
        /// <inheritdoc/>
        public async Task <(IReadOnlyList <RouteConfig> Routes, IReadOnlyList <ClusterConfig> Clusters)> DiscoverAsync(CancellationToken cancellation)
        {
            // Take a snapshot of current options and use that consistently for this execution.
            var options = _optionsMonitor.CurrentValue;

            _serviceFabricCaller.CleanUpExpired();

            var discoveredBackends = new Dictionary <string, ClusterConfig>(StringComparer.Ordinal);
            var discoveredRoutes   = new List <RouteConfig>();
            IEnumerable <ApplicationWrapper> applications;

            try
            {
                applications = await _serviceFabricCaller.GetApplicationListAsync(cancellation);
            }
            catch (OperationCanceledException) when(cancellation.IsCancellationRequested)
            {
                throw;
            }
            catch (Exception ex) // TODO: davidni: not fatal?
            {
                // The serviceFabricCaller does their best effort to use LKG information, nothing we can do at this point
                Log.GettingApplicationFailed(_logger, ex);
                applications = Enumerable.Empty <ApplicationWrapper>();
            }

            foreach (var application in applications)
            {
                IEnumerable <ServiceWrapper> services;

                try
                {
                    services = await _serviceFabricCaller.GetServiceListAsync(application.ApplicationName, cancellation);
                }
                catch (OperationCanceledException) when(cancellation.IsCancellationRequested)
                {
                    throw;
                }
                catch (Exception ex) // TODO: davidni: not fatal?
                {
                    Log.GettingServiceFailed(_logger, application.ApplicationName, ex);
                    continue;
                }

                foreach (var service in services)
                {
                    try
                    {
                        var serviceExtensionLabels = await _serviceFabricExtensionConfigProvider.GetExtensionLabelsAsync(application, service, cancellation);

                        // If this service wants to use us as the proxy
                        if (serviceExtensionLabels.GetValueOrDefault("YARP.Enable", null) != "true")
                        {
                            // Skip this service
                            continue;
                        }

                        var destinations = await DiscoverDestinationsAsync(options, service, serviceExtensionLabels, cancellation);

                        var cluster = LabelsParser.BuildCluster(service.ServiceName, serviceExtensionLabels, destinations);
                        var clusterValidationErrors = await _configValidator.ValidateClusterAsync(cluster);

                        if (clusterValidationErrors.Count > 0)
                        {
                            throw new ConfigException($"Skipping cluster id '{cluster.ClusterId} due to validation errors.", new AggregateException(clusterValidationErrors));
                        }

                        if (!discoveredBackends.TryAdd(cluster.ClusterId, cluster))
                        {
                            throw new ConfigException($"Duplicated cluster id '{cluster.ClusterId}'. Skipping repeated definition, service '{service.ServiceName}'");
                        }

                        var routes = LabelsParser.BuildRoutes(service.ServiceName, serviceExtensionLabels);
                        var routeValidationErrors = new List <Exception>();
                        foreach (var route in routes)
                        {
                            routeValidationErrors.AddRange(await _configValidator.ValidateRouteAsync(route));
                        }

                        if (routeValidationErrors.Count > 0)
                        {
                            // Don't add ANY routes if even a single one is bad. Trying to add partial routes
                            // could lead to unexpected results (e.g. a typo in the configuration of higher-priority route
                            // could lead to a lower-priority route being selected for requests it should not be handling).
                            throw new ConfigException($"Skipping ALL routes for cluster id '{cluster.ClusterId} due to validation errors.", new AggregateException(routeValidationErrors));
                        }

                        discoveredRoutes.AddRange(routes);

                        ReportServiceHealth(options, service.ServiceName, HealthState.Ok, $"Successfully built cluster '{cluster.ClusterId}' with {routes.Count} routes.");
                    }
                    catch (ConfigException ex)
                    {
                        // User error
                        Log.InvalidServiceConfig(_logger, service.ServiceName, ex);

                        // TODO: emit Error health report once we are able to detect config issues *during* (as opposed to *after*) a target service upgrade.
                        // Proactive Error health report would trigger a rollback of the target service as desired. However, an Error report after rhe fact
                        // will NOT cause a rollback and will prevent the target service from performing subsequent monitored upgrades to mitigate, making things worse.
                        ReportServiceHealth(options, service.ServiceName, HealthState.Warning, $"Could not load service configuration: {ex.Message}.");
                    }
                    catch (Exception ex) // TODO: davidni: not fatal?
                    {
                        // Not user's problem
                        Log.ErrorLoadingServiceConfig(_logger, service.ServiceName, ex);
                    }
                }
            }

            Log.ServiceDiscovered(_logger, discoveredBackends.Count, discoveredRoutes.Count);
            return(discoveredRoutes, discoveredBackends.Values.ToList());
        }