d3fd9c14b6eb64f50ec97731caaabf235d47831b
[platform/upstream/grpc.git] / src / core / ext / xds / xds_api.h
1 /*
2  *
3  * Copyright 2018 gRPC authors.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *     http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  *
17  */
18
19 #ifndef GRPC_CORE_EXT_XDS_XDS_API_H
20 #define GRPC_CORE_EXT_XDS_XDS_API_H
21
22 #include <grpc/support/port_platform.h>
23
24 #include <stdint.h>
25
26 #include <set>
27
28 #include "absl/container/inlined_vector.h"
29 #include "absl/types/optional.h"
30 #include "re2/re2.h"
31
32 #include "upb/def.hpp"
33
34 #include <grpc/slice_buffer.h>
35
36 #include "envoy/admin/v3/config_dump.upb.h"
37 #include "src/core/ext/filters/client_channel/server_address.h"
38 #include "src/core/ext/xds/xds_bootstrap.h"
39 #include "src/core/ext/xds/xds_client_stats.h"
40 #include "src/core/ext/xds/xds_http_filters.h"
41 #include "src/core/lib/channel/status_util.h"
42 #include "src/core/lib/matchers/matchers.h"
43
44 namespace grpc_core {
45
46 // TODO(yashykt): Check to see if xDS security is enabled. This will be
47 // removed once this feature is fully integration-tested and enabled by
48 // default.
49 bool XdsSecurityEnabled();
50
51 class XdsClient;
52
53 class XdsApi {
54  public:
55   static const char* kLdsTypeUrl;
56   static const char* kRdsTypeUrl;
57   static const char* kCdsTypeUrl;
58   static const char* kEdsTypeUrl;
59
60   struct Duration {
61     int64_t seconds = 0;
62     int32_t nanos = 0;
63     bool operator==(const Duration& other) const {
64       return seconds == other.seconds && nanos == other.nanos;
65     }
66     std::string ToString() const {
67       return absl::StrFormat("Duration seconds: %ld, nanos %d", seconds, nanos);
68     }
69   };
70
71   using TypedPerFilterConfig =
72       std::map<std::string, XdsHttpFilterImpl::FilterConfig>;
73
74   // TODO(donnadionne): When we can use absl::variant<>, consider using that
75   // for: PathMatcher, HeaderMatcher, cluster_name and weighted_clusters
76   struct Route {
77     // Matchers for this route.
78     struct Matchers {
79       StringMatcher path_matcher;
80       std::vector<HeaderMatcher> header_matchers;
81       absl::optional<uint32_t> fraction_per_million;
82
83       bool operator==(const Matchers& other) const {
84         return path_matcher == other.path_matcher &&
85                header_matchers == other.header_matchers &&
86                fraction_per_million == other.fraction_per_million;
87       }
88       std::string ToString() const;
89     };
90
91     struct HashPolicy {
92       enum Type { HEADER, CHANNEL_ID };
93       Type type;
94       bool terminal = false;
95       // Fields used for type HEADER.
96       std::string header_name;
97       std::unique_ptr<RE2> regex = nullptr;
98       std::string regex_substitution;
99
100       HashPolicy() {}
101
102       // Copyable.
103       HashPolicy(const HashPolicy& other);
104       HashPolicy& operator=(const HashPolicy& other);
105
106       // Moveable.
107       HashPolicy(HashPolicy&& other) noexcept;
108       HashPolicy& operator=(HashPolicy&& other) noexcept;
109
110       bool operator==(const HashPolicy& other) const;
111       std::string ToString() const;
112     };
113     Matchers matchers;
114     std::vector<HashPolicy> hash_policies;
115
116     struct RetryPolicy {
117       internal::StatusCodeSet retry_on;
118       uint32_t num_retries;
119
120       struct RetryBackOff {
121         Duration base_interval;
122         Duration max_interval;
123
124         bool operator==(const RetryBackOff& other) const {
125           return base_interval == other.base_interval &&
126                  max_interval == other.max_interval;
127         }
128         std::string ToString() const;
129       };
130       RetryBackOff retry_back_off;
131
132       bool operator==(const RetryPolicy& other) const {
133         return (retry_on == other.retry_on &&
134                 num_retries == other.num_retries &&
135                 retry_back_off == other.retry_back_off);
136       }
137       std::string ToString() const;
138     };
139     absl::optional<RetryPolicy> retry_policy;
140
141     // Action for this route.
142     // TODO(roth): When we can use absl::variant<>, consider using that
143     // here, to enforce the fact that only one of the two fields can be set.
144     std::string cluster_name;
145     struct ClusterWeight {
146       std::string name;
147       uint32_t weight;
148       TypedPerFilterConfig typed_per_filter_config;
149
150       bool operator==(const ClusterWeight& other) const {
151         return name == other.name && weight == other.weight &&
152                typed_per_filter_config == other.typed_per_filter_config;
153       }
154       std::string ToString() const;
155     };
156     std::vector<ClusterWeight> weighted_clusters;
157     // Storing the timeout duration from route action:
158     // RouteAction.max_stream_duration.grpc_timeout_header_max or
159     // RouteAction.max_stream_duration.max_stream_duration if the former is
160     // not set.
161     absl::optional<Duration> max_stream_duration;
162
163     TypedPerFilterConfig typed_per_filter_config;
164
165     bool operator==(const Route& other) const {
166       return matchers == other.matchers && cluster_name == other.cluster_name &&
167              retry_policy == other.retry_policy &&
168              weighted_clusters == other.weighted_clusters &&
169              max_stream_duration == other.max_stream_duration &&
170              typed_per_filter_config == other.typed_per_filter_config;
171     }
172     std::string ToString() const;
173   };
174
175   struct RdsUpdate {
176     struct VirtualHost {
177       std::vector<std::string> domains;
178       std::vector<Route> routes;
179       TypedPerFilterConfig typed_per_filter_config;
180
181       bool operator==(const VirtualHost& other) const {
182         return domains == other.domains && routes == other.routes &&
183                typed_per_filter_config == other.typed_per_filter_config;
184       }
185     };
186
187     std::vector<VirtualHost> virtual_hosts;
188
189     bool operator==(const RdsUpdate& other) const {
190       return virtual_hosts == other.virtual_hosts;
191     }
192     std::string ToString() const;
193     VirtualHost* FindVirtualHostForDomain(const std::string& domain);
194   };
195
196   struct CommonTlsContext {
197     struct CertificateValidationContext {
198       std::vector<StringMatcher> match_subject_alt_names;
199
200       bool operator==(const CertificateValidationContext& other) const {
201         return match_subject_alt_names == other.match_subject_alt_names;
202       }
203
204       std::string ToString() const;
205       bool Empty() const;
206     };
207
208     struct CertificateProviderInstance {
209       std::string instance_name;
210       std::string certificate_name;
211
212       bool operator==(const CertificateProviderInstance& other) const {
213         return instance_name == other.instance_name &&
214                certificate_name == other.certificate_name;
215       }
216
217       std::string ToString() const;
218       bool Empty() const;
219     };
220
221     struct CombinedCertificateValidationContext {
222       CertificateValidationContext default_validation_context;
223       CertificateProviderInstance
224           validation_context_certificate_provider_instance;
225
226       bool operator==(const CombinedCertificateValidationContext& other) const {
227         return default_validation_context == other.default_validation_context &&
228                validation_context_certificate_provider_instance ==
229                    other.validation_context_certificate_provider_instance;
230       }
231
232       std::string ToString() const;
233       bool Empty() const;
234     };
235
236     CertificateProviderInstance tls_certificate_certificate_provider_instance;
237     CombinedCertificateValidationContext combined_validation_context;
238
239     bool operator==(const CommonTlsContext& other) const {
240       return tls_certificate_certificate_provider_instance ==
241                  other.tls_certificate_certificate_provider_instance &&
242              combined_validation_context == other.combined_validation_context;
243     }
244
245     std::string ToString() const;
246     bool Empty() const;
247   };
248
249   struct DownstreamTlsContext {
250     CommonTlsContext common_tls_context;
251     bool require_client_certificate = false;
252
253     bool operator==(const DownstreamTlsContext& other) const {
254       return common_tls_context == other.common_tls_context &&
255              require_client_certificate == other.require_client_certificate;
256     }
257
258     std::string ToString() const;
259     bool Empty() const;
260   };
261
262   // TODO(roth): When we can use absl::variant<>, consider using that
263   // here, to enforce the fact that only one of the two fields can be set.
264   struct LdsUpdate {
265     enum class ListenerType {
266       kTcpListener = 0,
267       kHttpApiListener,
268     } type;
269
270     struct HttpConnectionManager {
271       // The name to use in the RDS request.
272       std::string route_config_name;
273       // Storing the Http Connection Manager Common Http Protocol Option
274       // max_stream_duration
275       Duration http_max_stream_duration;
276       // The RouteConfiguration to use for this listener.
277       // Present only if it is inlined in the LDS response.
278       absl::optional<RdsUpdate> rds_update;
279
280       struct HttpFilter {
281         std::string name;
282         XdsHttpFilterImpl::FilterConfig config;
283
284         bool operator==(const HttpFilter& other) const {
285           return name == other.name && config == other.config;
286         }
287
288         std::string ToString() const;
289       };
290       std::vector<HttpFilter> http_filters;
291
292       bool operator==(const HttpConnectionManager& other) const {
293         return route_config_name == other.route_config_name &&
294                http_max_stream_duration == other.http_max_stream_duration &&
295                rds_update == other.rds_update &&
296                http_filters == other.http_filters;
297       }
298
299       std::string ToString() const;
300     };
301
302     // Populated for type=kHttpApiListener.
303     HttpConnectionManager http_connection_manager;
304
305     // Populated for type=kTcpListener.
306     // host:port listening_address set when type is kTcpListener
307     std::string address;
308
309     struct FilterChainData {
310       DownstreamTlsContext downstream_tls_context;
311       // This is in principle the filter list.
312       // We currently require exactly one filter, which is the HCM.
313       HttpConnectionManager http_connection_manager;
314
315       bool operator==(const FilterChainData& other) const {
316         return downstream_tls_context == other.downstream_tls_context &&
317                http_connection_manager == other.http_connection_manager;
318       }
319
320       std::string ToString() const;
321     } filter_chain_data;
322
323     // A multi-level map used to determine which filter chain to use for a given
324     // incoming connection. Determining the right filter chain for a given
325     // connection checks the following properties, in order:
326     // - destination port (never matched, so not present in map)
327     // - destination IP address
328     // - server name (never matched, so not present in map)
329     // - transport protocol (allows only "raw_buffer" or unset, prefers the
330     //   former, so only one of those two types is present in map)
331     // - application protocol (never matched, so not present in map)
332     // - connection source type (any, local or external)
333     // - source IP address
334     // - source port
335     // https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/listener/v3/listener_components.proto#config-listener-v3-filterchainmatch
336     // for more details
337     struct FilterChainMap {
338       struct FilterChainDataSharedPtr {
339         std::shared_ptr<FilterChainData> data;
340         bool operator==(const FilterChainDataSharedPtr& other) const {
341           return *data == *other.data;
342         }
343       };
344       struct CidrRange {
345         grpc_resolved_address address;
346         uint32_t prefix_len;
347
348         bool operator==(const CidrRange& other) const {
349           return memcmp(&address, &other.address, sizeof(address)) == 0 &&
350                  prefix_len == other.prefix_len;
351         }
352
353         std::string ToString() const;
354       };
355       using SourcePortsMap = std::map<uint16_t, FilterChainDataSharedPtr>;
356       struct SourceIp {
357         absl::optional<CidrRange> prefix_range;
358         SourcePortsMap ports_map;
359
360         bool operator==(const SourceIp& other) const {
361           return prefix_range == other.prefix_range &&
362                  ports_map == other.ports_map;
363         }
364       };
365       using SourceIpVector = std::vector<SourceIp>;
366       enum class ConnectionSourceType {
367         kAny = 0,
368         kSameIpOrLoopback,
369         kExternal
370       };
371       using ConnectionSourceTypesArray = std::array<SourceIpVector, 3>;
372       struct DestinationIp {
373         absl::optional<CidrRange> prefix_range;
374         // We always fail match on server name, so those filter chains are not
375         // included here.
376         ConnectionSourceTypesArray source_types_array;
377
378         bool operator==(const DestinationIp& other) const {
379           return prefix_range == other.prefix_range &&
380                  source_types_array == other.source_types_array;
381         }
382       };
383       // We always fail match on destination ports map
384       using DestinationIpVector = std::vector<DestinationIp>;
385       DestinationIpVector destination_ip_vector;
386
387       bool operator==(const FilterChainMap& other) const {
388         return destination_ip_vector == other.destination_ip_vector;
389       }
390
391       std::string ToString() const;
392     } filter_chain_map;
393
394     absl::optional<FilterChainData> default_filter_chain;
395
396     bool operator==(const LdsUpdate& other) const {
397       return http_connection_manager == other.http_connection_manager &&
398              address == other.address &&
399              filter_chain_map == other.filter_chain_map &&
400              default_filter_chain == other.default_filter_chain;
401     }
402
403     std::string ToString() const;
404   };
405
406   struct LdsResourceData {
407     LdsUpdate resource;
408     std::string serialized_proto;
409   };
410
411   using LdsUpdateMap = std::map<std::string /*server_name*/, LdsResourceData>;
412
413   struct RdsResourceData {
414     RdsUpdate resource;
415     std::string serialized_proto;
416   };
417
418   using RdsUpdateMap =
419       std::map<std::string /*route_config_name*/, RdsResourceData>;
420
421   struct CdsUpdate {
422     enum ClusterType { EDS, LOGICAL_DNS, AGGREGATE };
423     ClusterType cluster_type;
424     // For cluster type EDS.
425     // The name to use in the EDS request.
426     // If empty, the cluster name will be used.
427     std::string eds_service_name;
428     // For cluster type LOGICAL_DNS.
429     // The hostname to lookup in DNS.
430     std::string dns_hostname;
431     // For cluster type AGGREGATE.
432     // The prioritized list of cluster names.
433     std::vector<std::string> prioritized_cluster_names;
434
435     // Tls Context used by clients
436     CommonTlsContext common_tls_context;
437
438     // The LRS server to use for load reporting.
439     // If not set, load reporting will be disabled.
440     // If set to the empty string, will use the same server we obtained the CDS
441     // data from.
442     absl::optional<std::string> lrs_load_reporting_server_name;
443
444     // The LB policy to use (e.g., "ROUND_ROBIN" or "RING_HASH").
445     std::string lb_policy;
446     // Used for RING_HASH LB policy only.
447     uint64_t min_ring_size = 1024;
448     uint64_t max_ring_size = 8388608;
449     // Maximum number of outstanding requests can be made to the upstream
450     // cluster.
451     uint32_t max_concurrent_requests = 1024;
452
453     bool operator==(const CdsUpdate& other) const {
454       return cluster_type == other.cluster_type &&
455              eds_service_name == other.eds_service_name &&
456              dns_hostname == other.dns_hostname &&
457              prioritized_cluster_names == other.prioritized_cluster_names &&
458              common_tls_context == other.common_tls_context &&
459              lrs_load_reporting_server_name ==
460                  other.lrs_load_reporting_server_name &&
461              lb_policy == other.lb_policy &&
462              min_ring_size == other.min_ring_size &&
463              max_ring_size == other.max_ring_size &&
464              max_concurrent_requests == other.max_concurrent_requests;
465     }
466
467     std::string ToString() const;
468   };
469
470   struct CdsResourceData {
471     CdsUpdate resource;
472     std::string serialized_proto;
473   };
474
475   using CdsUpdateMap = std::map<std::string /*cluster_name*/, CdsResourceData>;
476
477   struct EdsUpdate {
478     struct Priority {
479       struct Locality {
480         RefCountedPtr<XdsLocalityName> name;
481         uint32_t lb_weight;
482         ServerAddressList endpoints;
483
484         bool operator==(const Locality& other) const {
485           return *name == *other.name && lb_weight == other.lb_weight &&
486                  endpoints == other.endpoints;
487         }
488         bool operator!=(const Locality& other) const {
489           return !(*this == other);
490         }
491         std::string ToString() const;
492       };
493
494       std::map<XdsLocalityName*, Locality, XdsLocalityName::Less> localities;
495
496       bool operator==(const Priority& other) const;
497       std::string ToString() const;
498     };
499     using PriorityList = absl::InlinedVector<Priority, 2>;
500
501     // There are two phases of accessing this class's content:
502     // 1. to initialize in the control plane combiner;
503     // 2. to use in the data plane combiner.
504     // So no additional synchronization is needed.
505     class DropConfig : public RefCounted<DropConfig> {
506      public:
507       struct DropCategory {
508         bool operator==(const DropCategory& other) const {
509           return name == other.name &&
510                  parts_per_million == other.parts_per_million;
511         }
512
513         std::string name;
514         const uint32_t parts_per_million;
515       };
516
517       using DropCategoryList = absl::InlinedVector<DropCategory, 2>;
518
519       void AddCategory(std::string name, uint32_t parts_per_million) {
520         drop_category_list_.emplace_back(
521             DropCategory{std::move(name), parts_per_million});
522         if (parts_per_million == 1000000) drop_all_ = true;
523       }
524
525       // The only method invoked from outside the WorkSerializer (used in
526       // the data plane).
527       bool ShouldDrop(const std::string** category_name) const;
528
529       const DropCategoryList& drop_category_list() const {
530         return drop_category_list_;
531       }
532
533       bool drop_all() const { return drop_all_; }
534
535       bool operator==(const DropConfig& other) const {
536         return drop_category_list_ == other.drop_category_list_;
537       }
538       bool operator!=(const DropConfig& other) const {
539         return !(*this == other);
540       }
541
542       std::string ToString() const;
543
544      private:
545       DropCategoryList drop_category_list_;
546       bool drop_all_ = false;
547     };
548
549     PriorityList priorities;
550     RefCountedPtr<DropConfig> drop_config;
551
552     bool operator==(const EdsUpdate& other) const {
553       return priorities == other.priorities &&
554              *drop_config == *other.drop_config;
555     }
556     std::string ToString() const;
557   };
558
559   struct EdsResourceData {
560     EdsUpdate resource;
561     std::string serialized_proto;
562   };
563
564   using EdsUpdateMap =
565       std::map<std::string /*eds_service_name*/, EdsResourceData>;
566
567   struct ClusterLoadReport {
568     XdsClusterDropStats::Snapshot dropped_requests;
569     std::map<RefCountedPtr<XdsLocalityName>, XdsClusterLocalityStats::Snapshot,
570              XdsLocalityName::Less>
571         locality_stats;
572     grpc_millis load_report_interval;
573   };
574   using ClusterLoadReportMap = std::map<
575       std::pair<std::string /*cluster_name*/, std::string /*eds_service_name*/>,
576       ClusterLoadReport>;
577
578   // The metadata of the xDS resource; used by the xDS config dump.
579   struct ResourceMetadata {
580     // Resource status from the view of a xDS client, which tells the
581     // synchronization status between the xDS client and the xDS server.
582     enum ClientResourceStatus {
583       // Client requested this resource but hasn't received any update from
584       // management server. The client will not fail requests, but will queue
585       // them
586       // until update arrives or the client times out waiting for the resource.
587       REQUESTED = 1,
588       // This resource has been requested by the client but has either not been
589       // delivered by the server or was previously delivered by the server and
590       // then subsequently removed from resources provided by the server.
591       DOES_NOT_EXIST,
592       // Client received this resource and replied with ACK.
593       ACKED,
594       // Client received this resource and replied with NACK.
595       NACKED
596     };
597
598     // The client status of this resource.
599     ClientResourceStatus client_status = REQUESTED;
600     // The serialized bytes of the last successfully updated raw xDS resource.
601     std::string serialized_proto;
602     // The timestamp when the resource was last successfully updated.
603     grpc_millis update_time = 0;
604     // The last successfully updated version of the resource.
605     std::string version;
606     // The rejected version string of the last failed update attempt.
607     std::string failed_version;
608     // Details about the last failed update attempt.
609     std::string failed_details;
610     // Timestamp of the last failed update attempt.
611     grpc_millis failed_update_time = 0;
612   };
613   using ResourceMetadataMap =
614       std::map<absl::string_view /*resource_name*/, const ResourceMetadata*>;
615   struct ResourceTypeMetadata {
616     absl::string_view version;
617     ResourceMetadataMap resource_metadata_map;
618   };
619   using ResourceTypeMetadataMap =
620       std::map<absl::string_view /*type_url*/, ResourceTypeMetadata>;
621   static_assert(static_cast<ResourceMetadata::ClientResourceStatus>(
622                     envoy_admin_v3_REQUESTED) ==
623                     ResourceMetadata::ClientResourceStatus::REQUESTED,
624                 "");
625   static_assert(static_cast<ResourceMetadata::ClientResourceStatus>(
626                     envoy_admin_v3_DOES_NOT_EXIST) ==
627                     ResourceMetadata::ClientResourceStatus::DOES_NOT_EXIST,
628                 "");
629   static_assert(static_cast<ResourceMetadata::ClientResourceStatus>(
630                     envoy_admin_v3_ACKED) ==
631                     ResourceMetadata::ClientResourceStatus::ACKED,
632                 "");
633   static_assert(static_cast<ResourceMetadata::ClientResourceStatus>(
634                     envoy_admin_v3_NACKED) ==
635                     ResourceMetadata::ClientResourceStatus::NACKED,
636                 "");
637
638   // If the response can't be parsed at the top level, the resulting
639   // type_url will be empty.
640   // If there is any other type of validation error, the parse_error
641   // field will be set to something other than GRPC_ERROR_NONE and the
642   // resource_names_failed field will be populated.
643   // Otherwise, one of the *_update_map fields will be populated, based
644   // on the type_url field.
645   struct AdsParseResult {
646     grpc_error_handle parse_error = GRPC_ERROR_NONE;
647     std::string version;
648     std::string nonce;
649     std::string type_url;
650     LdsUpdateMap lds_update_map;
651     RdsUpdateMap rds_update_map;
652     CdsUpdateMap cds_update_map;
653     EdsUpdateMap eds_update_map;
654     std::set<std::string> resource_names_failed;
655   };
656
657   XdsApi(XdsClient* client, TraceFlag* tracer, const XdsBootstrap::Node* node,
658          const CertificateProviderStore::PluginDefinitionMap* map);
659
660   // Creates an ADS request.
661   // Takes ownership of \a error.
662   grpc_slice CreateAdsRequest(const XdsBootstrap::XdsServer& server,
663                               const std::string& type_url,
664                               const std::set<absl::string_view>& resource_names,
665                               const std::string& version,
666                               const std::string& nonce, grpc_error_handle error,
667                               bool populate_node);
668
669   // Parses an ADS response.
670   AdsParseResult ParseAdsResponse(
671       const XdsBootstrap::XdsServer& server, const grpc_slice& encoded_response,
672       const std::set<absl::string_view>& expected_listener_names,
673       const std::set<absl::string_view>& expected_route_configuration_names,
674       const std::set<absl::string_view>& expected_cluster_names,
675       const std::set<absl::string_view>& expected_eds_service_names);
676
677   // Creates an initial LRS request.
678   grpc_slice CreateLrsInitialRequest(const XdsBootstrap::XdsServer& server);
679
680   // Creates an LRS request sending a client-side load report.
681   grpc_slice CreateLrsRequest(ClusterLoadReportMap cluster_load_report_map);
682
683   // Parses the LRS response and returns \a
684   // load_reporting_interval for client-side load reporting. If there is any
685   // error, the output config is invalid.
686   grpc_error_handle ParseLrsResponse(const grpc_slice& encoded_response,
687                                      bool* send_all_clusters,
688                                      std::set<std::string>* cluster_names,
689                                      grpc_millis* load_reporting_interval);
690
691   // Assemble the client config proto message and return the serialized result.
692   std::string AssembleClientConfig(
693       const ResourceTypeMetadataMap& resource_type_metadata_map);
694
695  private:
696   XdsClient* client_;
697   TraceFlag* tracer_;
698   const XdsBootstrap::Node* node_;  // Do not own.
699   const CertificateProviderStore::PluginDefinitionMap*
700       certificate_provider_definition_map_;  // Do not own.
701   upb::SymbolTable symtab_;
702   const std::string build_version_;
703   const std::string user_agent_name_;
704   const std::string user_agent_version_;
705 };
706
707 }  // namespace grpc_core
708
709 #endif /* GRPC_CORE_EXT_XDS_XDS_API_H */