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); }
/// <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()); }