1 //
2 // Copyright 2019 gRPC authors.
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 // http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15 //
16
17 #include <grpc/support/port_platform.h>
18
19 #include "src/core/ext/xds/xds_bootstrap.h"
20
21 #include <vector>
22
23 #include <errno.h>
24 #include <stdlib.h>
25
26 #include "absl/strings/str_cat.h"
27 #include "absl/strings/str_format.h"
28 #include "absl/strings/str_join.h"
29 #include "absl/strings/string_view.h"
30
31 #include "src/core/ext/xds/certificate_provider_registry.h"
32 #include "src/core/ext/xds/xds_api.h"
33 #include "src/core/lib/gpr/env.h"
34 #include "src/core/lib/gpr/string.h"
35 #include "src/core/lib/iomgr/load_file.h"
36 #include "src/core/lib/security/credentials/credentials.h"
37 #include "src/core/lib/security/credentials/fake/fake_credentials.h"
38 #include "src/core/lib/slice/slice_internal.h"
39
40 namespace grpc_core {
41
42 //
43 // XdsChannelCredsRegistry
44 //
45
IsSupported(const std::string & creds_type)46 bool XdsChannelCredsRegistry::IsSupported(const std::string& creds_type) {
47 return creds_type == "google_default" || creds_type == "insecure" ||
48 creds_type == "fake";
49 }
50
IsValidConfig(const std::string &,const Json &)51 bool XdsChannelCredsRegistry::IsValidConfig(const std::string& /*creds_type*/,
52 const Json& /*config*/) {
53 // Currently, none of the creds types actually take a config, but we
54 // ignore whatever might be specified in the bootstrap file for
55 // forward compatibility reasons.
56 return true;
57 }
58
59 RefCountedPtr<grpc_channel_credentials>
MakeChannelCreds(const std::string & creds_type,const Json &)60 XdsChannelCredsRegistry::MakeChannelCreds(const std::string& creds_type,
61 const Json& /*config*/) {
62 if (creds_type == "google_default") {
63 return grpc_google_default_credentials_create(nullptr);
64 } else if (creds_type == "insecure") {
65 return grpc_insecure_credentials_create();
66 } else if (creds_type == "fake") {
67 return grpc_fake_transport_security_credentials_create();
68 }
69 return nullptr;
70 }
71
72 //
73 // XdsBootstrap::XdsServer
74 //
75
ShouldUseV3() const76 bool XdsBootstrap::XdsServer::ShouldUseV3() const {
77 return server_features.find("xds_v3") != server_features.end();
78 }
79
80 //
81 // XdsBootstrap
82 //
83
84 namespace {
85
BootstrapString(const XdsBootstrap & bootstrap)86 std::string BootstrapString(const XdsBootstrap& bootstrap) {
87 std::vector<std::string> parts;
88 if (bootstrap.node() != nullptr) {
89 parts.push_back(absl::StrFormat(
90 "node={\n"
91 " id=\"%s\",\n"
92 " cluster=\"%s\",\n"
93 " locality={\n"
94 " region=\"%s\",\n"
95 " zone=\"%s\",\n"
96 " subzone=\"%s\"\n"
97 " },\n"
98 " metadata=%s,\n"
99 "},\n",
100 bootstrap.node()->id, bootstrap.node()->cluster,
101 bootstrap.node()->locality_region, bootstrap.node()->locality_zone,
102 bootstrap.node()->locality_subzone, bootstrap.node()->metadata.Dump()));
103 }
104 parts.push_back(absl::StrFormat(
105 "servers=[\n"
106 " {\n"
107 " uri=\"%s\",\n"
108 " creds_type=%s,\n",
109 bootstrap.server().server_uri, bootstrap.server().channel_creds_type));
110 if (bootstrap.server().channel_creds_config.type() != Json::Type::JSON_NULL) {
111 parts.push_back(
112 absl::StrFormat(" creds_config=%s,",
113 bootstrap.server().channel_creds_config.Dump()));
114 }
115 if (!bootstrap.server().server_features.empty()) {
116 parts.push_back(absl::StrCat(
117 " server_features=[",
118 absl::StrJoin(bootstrap.server().server_features, ", "), "],\n"));
119 }
120 parts.push_back(" }\n],\n");
121 parts.push_back("certificate_providers={\n");
122 for (const auto& entry : bootstrap.certificate_providers()) {
123 parts.push_back(
124 absl::StrFormat(" %s={\n"
125 " plugin_name=%s\n"
126 " config=%s\n"
127 " },\n",
128 entry.first, entry.second.plugin_name,
129 entry.second.config->ToString()));
130 }
131 parts.push_back("}");
132 return absl::StrJoin(parts, "");
133 }
134
ParseJsonAndCreate(XdsClient * client,TraceFlag * tracer,absl::string_view json_string,absl::string_view bootstrap_source,grpc_error ** error)135 std::unique_ptr<XdsBootstrap> ParseJsonAndCreate(
136 XdsClient* client, TraceFlag* tracer, absl::string_view json_string,
137 absl::string_view bootstrap_source, grpc_error** error) {
138 Json json = Json::Parse(json_string, error);
139 if (*error != GRPC_ERROR_NONE) {
140 grpc_error* error_out = GRPC_ERROR_CREATE_REFERENCING_FROM_COPIED_STRING(
141 absl::StrCat("Failed to parse bootstrap from ", bootstrap_source)
142 .c_str(),
143 error, 1);
144 GRPC_ERROR_UNREF(*error);
145 *error = error_out;
146 return nullptr;
147 }
148 std::unique_ptr<XdsBootstrap> result =
149 absl::make_unique<XdsBootstrap>(std::move(json), error);
150 if (*error == GRPC_ERROR_NONE && GRPC_TRACE_FLAG_ENABLED(*tracer)) {
151 gpr_log(GPR_INFO,
152 "[xds_client %p] Bootstrap config for creating xds client:\n%s",
153 client, BootstrapString(*result).c_str());
154 }
155 return result;
156 }
157
158 } // namespace
159
Create(XdsClient * client,TraceFlag * tracer,const char * fallback_config,grpc_error ** error)160 std::unique_ptr<XdsBootstrap> XdsBootstrap::Create(XdsClient* client,
161 TraceFlag* tracer,
162 const char* fallback_config,
163 grpc_error** error) {
164 // First, try GRPC_XDS_BOOTSTRAP env var.
165 grpc_core::UniquePtr<char> path(gpr_getenv("GRPC_XDS_BOOTSTRAP"));
166 if (path != nullptr) {
167 if (GRPC_TRACE_FLAG_ENABLED(*tracer)) {
168 gpr_log(GPR_INFO,
169 "[xds_client %p] Got bootstrap file location from "
170 "GRPC_XDS_BOOTSTRAP environment variable: %s",
171 client, path.get());
172 }
173 grpc_slice contents;
174 *error =
175 grpc_load_file(path.get(), /*add_null_terminator=*/true, &contents);
176 if (*error != GRPC_ERROR_NONE) return nullptr;
177 absl::string_view contents_str_view = StringViewFromSlice(contents);
178 if (GRPC_TRACE_FLAG_ENABLED(*tracer)) {
179 gpr_log(GPR_DEBUG, "[xds_client %p] Bootstrap file contents: %s", client,
180 std::string(contents_str_view).c_str());
181 }
182 std::string bootstrap_source = absl::StrCat("file ", path.get());
183 auto result = ParseJsonAndCreate(client, tracer, contents_str_view,
184 bootstrap_source, error);
185 grpc_slice_unref_internal(contents);
186 return result;
187 }
188 // Next, try GRPC_XDS_BOOTSTRAP_CONFIG env var.
189 grpc_core::UniquePtr<char> env_config(
190 gpr_getenv("GRPC_XDS_BOOTSTRAP_CONFIG"));
191 if (env_config != nullptr) {
192 return ParseJsonAndCreate(client, tracer, env_config.get(),
193 "GRPC_XDS_BOOTSTRAP_CONFIG env var", error);
194 }
195 // Finally, try fallback config.
196 if (fallback_config != nullptr) {
197 return ParseJsonAndCreate(client, tracer, fallback_config,
198 "fallback config", error);
199 }
200 // No bootstrap config found.
201 *error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
202 "Environment variables GRPC_XDS_BOOTSTRAP or GRPC_XDS_BOOTSTRAP_CONFIG "
203 "not defined");
204 return nullptr;
205 }
206
XdsBootstrap(Json json,grpc_error ** error)207 XdsBootstrap::XdsBootstrap(Json json, grpc_error** error) {
208 if (json.type() != Json::Type::OBJECT) {
209 *error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
210 "malformed JSON in bootstrap file");
211 return;
212 }
213 std::vector<grpc_error*> error_list;
214 auto it = json.mutable_object()->find("xds_servers");
215 if (it == json.mutable_object()->end()) {
216 error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
217 "\"xds_servers\" field not present"));
218 } else if (it->second.type() != Json::Type::ARRAY) {
219 error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
220 "\"xds_servers\" field is not an array"));
221 } else {
222 grpc_error* parse_error = ParseXdsServerList(&it->second);
223 if (parse_error != GRPC_ERROR_NONE) error_list.push_back(parse_error);
224 }
225 it = json.mutable_object()->find("node");
226 if (it != json.mutable_object()->end()) {
227 if (it->second.type() != Json::Type::OBJECT) {
228 error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
229 "\"node\" field is not an object"));
230 } else {
231 grpc_error* parse_error = ParseNode(&it->second);
232 if (parse_error != GRPC_ERROR_NONE) error_list.push_back(parse_error);
233 }
234 }
235 if (XdsSecurityEnabled()) {
236 it = json.mutable_object()->find("certificate_providers");
237 if (it != json.mutable_object()->end()) {
238 if (it->second.type() != Json::Type::OBJECT) {
239 error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
240 "\"certificate_providers\" field is not an object"));
241 } else {
242 grpc_error* parse_error = ParseCertificateProviders(&it->second);
243 if (parse_error != GRPC_ERROR_NONE) error_list.push_back(parse_error);
244 }
245 }
246 }
247 *error = GRPC_ERROR_CREATE_FROM_VECTOR("errors parsing xds bootstrap file",
248 &error_list);
249 }
250
ParseXdsServerList(Json * json)251 grpc_error* XdsBootstrap::ParseXdsServerList(Json* json) {
252 std::vector<grpc_error*> error_list;
253 for (size_t i = 0; i < json->mutable_array()->size(); ++i) {
254 Json& child = json->mutable_array()->at(i);
255 if (child.type() != Json::Type::OBJECT) {
256 error_list.push_back(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
257 absl::StrCat("array element ", i, " is not an object").c_str()));
258 } else {
259 grpc_error* parse_error = ParseXdsServer(&child, i);
260 if (parse_error != GRPC_ERROR_NONE) error_list.push_back(parse_error);
261 }
262 }
263 return GRPC_ERROR_CREATE_FROM_VECTOR("errors parsing \"xds_servers\" array",
264 &error_list);
265 }
266
ParseXdsServer(Json * json,size_t idx)267 grpc_error* XdsBootstrap::ParseXdsServer(Json* json, size_t idx) {
268 std::vector<grpc_error*> error_list;
269 servers_.emplace_back();
270 XdsServer& server = servers_[servers_.size() - 1];
271 auto it = json->mutable_object()->find("server_uri");
272 if (it == json->mutable_object()->end()) {
273 error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
274 "\"server_uri\" field not present"));
275 } else if (it->second.type() != Json::Type::STRING) {
276 error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
277 "\"server_uri\" field is not a string"));
278 } else {
279 server.server_uri = std::move(*it->second.mutable_string_value());
280 }
281 it = json->mutable_object()->find("channel_creds");
282 if (it == json->mutable_object()->end()) {
283 error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
284 "\"channel_creds\" field not present"));
285 } else if (it->second.type() != Json::Type::ARRAY) {
286 error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
287 "\"channel_creds\" field is not an array"));
288 } else {
289 grpc_error* parse_error = ParseChannelCredsArray(&it->second, &server);
290 if (parse_error != GRPC_ERROR_NONE) error_list.push_back(parse_error);
291 }
292 it = json->mutable_object()->find("server_features");
293 if (it != json->mutable_object()->end()) {
294 if (it->second.type() != Json::Type::ARRAY) {
295 error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
296 "\"server_features\" field is not an array"));
297 } else {
298 grpc_error* parse_error = ParseServerFeaturesArray(&it->second, &server);
299 if (parse_error != GRPC_ERROR_NONE) error_list.push_back(parse_error);
300 }
301 }
302 // Can't use GRPC_ERROR_CREATE_FROM_VECTOR() here, because the error
303 // string is not static in this case.
304 if (error_list.empty()) return GRPC_ERROR_NONE;
305 grpc_error* error = GRPC_ERROR_CREATE_FROM_COPIED_STRING(
306 absl::StrCat("errors parsing index ", idx).c_str());
307 for (size_t i = 0; i < error_list.size(); ++i) {
308 error = grpc_error_add_child(error, error_list[i]);
309 }
310 return error;
311 }
312
ParseChannelCredsArray(Json * json,XdsServer * server)313 grpc_error* XdsBootstrap::ParseChannelCredsArray(Json* json,
314 XdsServer* server) {
315 std::vector<grpc_error*> error_list;
316 for (size_t i = 0; i < json->mutable_array()->size(); ++i) {
317 Json& child = json->mutable_array()->at(i);
318 if (child.type() != Json::Type::OBJECT) {
319 error_list.push_back(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
320 absl::StrCat("array element ", i, " is not an object").c_str()));
321 } else {
322 grpc_error* parse_error = ParseChannelCreds(&child, i, server);
323 if (parse_error != GRPC_ERROR_NONE) error_list.push_back(parse_error);
324 }
325 }
326 if (server->channel_creds_type.empty()) {
327 error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
328 "no known creds type found in \"channel_creds\""));
329 }
330 return GRPC_ERROR_CREATE_FROM_VECTOR("errors parsing \"channel_creds\" array",
331 &error_list);
332 }
333
ParseChannelCreds(Json * json,size_t idx,XdsServer * server)334 grpc_error* XdsBootstrap::ParseChannelCreds(Json* json, size_t idx,
335 XdsServer* server) {
336 std::vector<grpc_error*> error_list;
337 std::string type;
338 auto it = json->mutable_object()->find("type");
339 if (it == json->mutable_object()->end()) {
340 error_list.push_back(
341 GRPC_ERROR_CREATE_FROM_STATIC_STRING("\"type\" field not present"));
342 } else if (it->second.type() != Json::Type::STRING) {
343 error_list.push_back(
344 GRPC_ERROR_CREATE_FROM_STATIC_STRING("\"type\" field is not a string"));
345 } else {
346 type = std::move(*it->second.mutable_string_value());
347 }
348 Json config;
349 it = json->mutable_object()->find("config");
350 if (it != json->mutable_object()->end()) {
351 if (it->second.type() != Json::Type::OBJECT) {
352 error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
353 "\"config\" field is not an object"));
354 } else {
355 config = std::move(it->second);
356 }
357 }
358 // Select the first channel creds type that we support.
359 if (server->channel_creds_type.empty() &&
360 XdsChannelCredsRegistry::IsSupported(type)) {
361 if (!XdsChannelCredsRegistry::IsValidConfig(type, config)) {
362 error_list.push_back(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
363 absl::StrCat("invalid config for channel creds type \"", type, "\"")
364 .c_str()));
365 }
366 server->channel_creds_type = std::move(type);
367 server->channel_creds_config = std::move(config);
368 }
369 // Can't use GRPC_ERROR_CREATE_FROM_VECTOR() here, because the error
370 // string is not static in this case.
371 if (error_list.empty()) return GRPC_ERROR_NONE;
372 grpc_error* error = GRPC_ERROR_CREATE_FROM_COPIED_STRING(
373 absl::StrCat("errors parsing index ", idx).c_str());
374 for (size_t i = 0; i < error_list.size(); ++i) {
375 error = grpc_error_add_child(error, error_list[i]);
376 }
377 return error;
378 }
379
ParseServerFeaturesArray(Json * json,XdsServer * server)380 grpc_error* XdsBootstrap::ParseServerFeaturesArray(Json* json,
381 XdsServer* server) {
382 std::vector<grpc_error*> error_list;
383 for (size_t i = 0; i < json->mutable_array()->size(); ++i) {
384 Json& child = json->mutable_array()->at(i);
385 if (child.type() == Json::Type::STRING &&
386 child.string_value() == "xds_v3") {
387 server->server_features.insert(std::move(*child.mutable_string_value()));
388 }
389 }
390 return GRPC_ERROR_CREATE_FROM_VECTOR(
391 "errors parsing \"server_features\" array", &error_list);
392 }
393
ParseNode(Json * json)394 grpc_error* XdsBootstrap::ParseNode(Json* json) {
395 std::vector<grpc_error*> error_list;
396 node_ = absl::make_unique<Node>();
397 auto it = json->mutable_object()->find("id");
398 if (it != json->mutable_object()->end()) {
399 if (it->second.type() != Json::Type::STRING) {
400 error_list.push_back(
401 GRPC_ERROR_CREATE_FROM_STATIC_STRING("\"id\" field is not a string"));
402 } else {
403 node_->id = std::move(*it->second.mutable_string_value());
404 }
405 }
406 it = json->mutable_object()->find("cluster");
407 if (it != json->mutable_object()->end()) {
408 if (it->second.type() != Json::Type::STRING) {
409 error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
410 "\"cluster\" field is not a string"));
411 } else {
412 node_->cluster = std::move(*it->second.mutable_string_value());
413 }
414 }
415 it = json->mutable_object()->find("locality");
416 if (it != json->mutable_object()->end()) {
417 if (it->second.type() != Json::Type::OBJECT) {
418 error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
419 "\"locality\" field is not an object"));
420 } else {
421 grpc_error* parse_error = ParseLocality(&it->second);
422 if (parse_error != GRPC_ERROR_NONE) error_list.push_back(parse_error);
423 }
424 }
425 it = json->mutable_object()->find("metadata");
426 if (it != json->mutable_object()->end()) {
427 if (it->second.type() != Json::Type::OBJECT) {
428 error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
429 "\"metadata\" field is not an object"));
430 } else {
431 node_->metadata = std::move(it->second);
432 }
433 }
434 return GRPC_ERROR_CREATE_FROM_VECTOR("errors parsing \"node\" object",
435 &error_list);
436 }
437
ParseLocality(Json * json)438 grpc_error* XdsBootstrap::ParseLocality(Json* json) {
439 std::vector<grpc_error*> error_list;
440 auto it = json->mutable_object()->find("region");
441 if (it != json->mutable_object()->end()) {
442 if (it->second.type() != Json::Type::STRING) {
443 error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
444 "\"region\" field is not a string"));
445 } else {
446 node_->locality_region = std::move(*it->second.mutable_string_value());
447 }
448 }
449 it = json->mutable_object()->find("zone");
450 if (it != json->mutable_object()->end()) {
451 if (it->second.type() != Json::Type::STRING) {
452 error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
453 "\"zone\" field is not a string"));
454 } else {
455 node_->locality_zone = std::move(*it->second.mutable_string_value());
456 }
457 }
458 it = json->mutable_object()->find("subzone");
459 if (it != json->mutable_object()->end()) {
460 if (it->second.type() != Json::Type::STRING) {
461 error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
462 "\"subzone\" field is not a string"));
463 } else {
464 node_->locality_subzone = std::move(*it->second.mutable_string_value());
465 }
466 }
467 return GRPC_ERROR_CREATE_FROM_VECTOR("errors parsing \"locality\" object",
468 &error_list);
469 }
470
ParseCertificateProviders(Json * json)471 grpc_error* XdsBootstrap::ParseCertificateProviders(Json* json) {
472 std::vector<grpc_error*> error_list;
473 for (auto& certificate_provider : *(json->mutable_object())) {
474 if (certificate_provider.second.type() != Json::Type::OBJECT) {
475 error_list.push_back(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
476 absl::StrCat("element \"", certificate_provider.first,
477 "\" is not an object")
478 .c_str()));
479 } else {
480 grpc_error* parse_error = ParseCertificateProvider(
481 certificate_provider.first, &certificate_provider.second);
482 if (parse_error != GRPC_ERROR_NONE) error_list.push_back(parse_error);
483 }
484 }
485 return GRPC_ERROR_CREATE_FROM_VECTOR(
486 "errors parsing \"certificate_providers\" object", &error_list);
487 }
488
ParseCertificateProvider(const std::string & instance_name,Json * certificate_provider_json)489 grpc_error* XdsBootstrap::ParseCertificateProvider(
490 const std::string& instance_name, Json* certificate_provider_json) {
491 std::vector<grpc_error*> error_list;
492 auto it = certificate_provider_json->mutable_object()->find("plugin_name");
493 if (it == certificate_provider_json->mutable_object()->end()) {
494 error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
495 "\"plugin_name\" field not present"));
496 } else if (it->second.type() != Json::Type::STRING) {
497 error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
498 "\"plugin_name\" field is not a string"));
499 } else {
500 std::string plugin_name = std::move(*(it->second.mutable_string_value()));
501 CertificateProviderFactory* factory =
502 CertificateProviderRegistry::LookupCertificateProviderFactory(
503 plugin_name);
504 if (factory != nullptr) {
505 RefCountedPtr<CertificateProviderFactory::Config> config;
506 it = certificate_provider_json->mutable_object()->find("config");
507 if (it != certificate_provider_json->mutable_object()->end()) {
508 if (it->second.type() != Json::Type::OBJECT) {
509 error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
510 "\"config\" field is not an object"));
511 } else {
512 grpc_error* parse_error = GRPC_ERROR_NONE;
513 config = factory->CreateCertificateProviderConfig(it->second,
514 &parse_error);
515 if (parse_error != GRPC_ERROR_NONE) error_list.push_back(parse_error);
516 }
517 } else {
518 // "config" is an optional field, so create an empty JSON object.
519 grpc_error* parse_error = GRPC_ERROR_NONE;
520 config = factory->CreateCertificateProviderConfig(Json::Object(),
521 &parse_error);
522 if (parse_error != GRPC_ERROR_NONE) error_list.push_back(parse_error);
523 }
524 certificate_providers_.insert(
525 {instance_name, {std::move(plugin_name), std::move(config)}});
526 }
527 }
528 // Can't use GRPC_ERROR_CREATE_FROM_VECTOR() here, because the error
529 // string is not static in this case.
530 if (error_list.empty()) return GRPC_ERROR_NONE;
531 grpc_error* error = GRPC_ERROR_CREATE_FROM_COPIED_STRING(
532 absl::StrCat("errors parsing element \"", instance_name, "\"").c_str());
533 for (size_t i = 0; i < error_list.size(); ++i) {
534 error = grpc_error_add_child(error, error_list[i]);
535 }
536 return error;
537 }
538
539 } // namespace grpc_core
540