• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2021 The 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 package io.grpc.xds;
18 
19 import com.google.common.annotations.VisibleForTesting;
20 import com.google.common.base.Strings;
21 import com.google.common.collect.ImmutableList;
22 import com.google.common.collect.ImmutableMap;
23 import io.grpc.ChannelCredentials;
24 import io.grpc.InternalLogId;
25 import io.grpc.internal.GrpcUtil;
26 import io.grpc.internal.GrpcUtil.GrpcBuildVersion;
27 import io.grpc.internal.JsonParser;
28 import io.grpc.internal.JsonUtil;
29 import io.grpc.xds.EnvoyProtoData.Node;
30 import io.grpc.xds.XdsLogger.XdsLogLevel;
31 import java.io.IOException;
32 import java.nio.charset.StandardCharsets;
33 import java.nio.file.Files;
34 import java.nio.file.Paths;
35 import java.util.HashMap;
36 import java.util.List;
37 import java.util.Map;
38 import javax.annotation.Nullable;
39 
40 /**
41  * A {@link Bootstrapper} implementation that reads xDS configurations from local file system.
42  */
43 class BootstrapperImpl extends Bootstrapper {
44 
45   private static final String BOOTSTRAP_PATH_SYS_ENV_VAR = "GRPC_XDS_BOOTSTRAP";
46   @VisibleForTesting
47   static String bootstrapPathFromEnvVar = System.getenv(BOOTSTRAP_PATH_SYS_ENV_VAR);
48   private static final String BOOTSTRAP_PATH_SYS_PROPERTY = "io.grpc.xds.bootstrap";
49   @VisibleForTesting
50   static String bootstrapPathFromSysProp = System.getProperty(BOOTSTRAP_PATH_SYS_PROPERTY);
51   private static final String BOOTSTRAP_CONFIG_SYS_ENV_VAR = "GRPC_XDS_BOOTSTRAP_CONFIG";
52   @VisibleForTesting
53   static String bootstrapConfigFromEnvVar = System.getenv(BOOTSTRAP_CONFIG_SYS_ENV_VAR);
54   private static final String BOOTSTRAP_CONFIG_SYS_PROPERTY = "io.grpc.xds.bootstrapConfig";
55   @VisibleForTesting
56   static String bootstrapConfigFromSysProp = System.getProperty(BOOTSTRAP_CONFIG_SYS_PROPERTY);
57 
58   // Feature-gating environment variables.
59   static boolean enableFederation =
60       Strings.isNullOrEmpty(System.getenv("GRPC_EXPERIMENTAL_XDS_FEDERATION"))
61           || Boolean.parseBoolean(System.getenv("GRPC_EXPERIMENTAL_XDS_FEDERATION"));
62 
63   // Client features.
64   @VisibleForTesting
65   static final String CLIENT_FEATURE_DISABLE_OVERPROVISIONING =
66       "envoy.lb.does_not_support_overprovisioning";
67   @VisibleForTesting
68   static final String CLIENT_FEATURE_RESOURCE_IN_SOTW = "xds.config.resource-in-sotw";
69 
70   // Server features.
71   private static final String SERVER_FEATURE_IGNORE_RESOURCE_DELETION = "ignore_resource_deletion";
72 
73   private final XdsLogger logger;
74   private FileReader reader = LocalFileReader.INSTANCE;
75 
BootstrapperImpl()76   public BootstrapperImpl() {
77     logger = XdsLogger.withLogId(InternalLogId.allocate("bootstrapper", null));
78   }
79 
80   /**
81    * Reads and parses bootstrap config. Searches the config (or file of config) with the
82    * following order:
83    *
84    * <ol>
85    *   <li>A filesystem path defined by environment variable "GRPC_XDS_BOOTSTRAP"</li>
86    *   <li>A filesystem path defined by Java System Property "io.grpc.xds.bootstrap"</li>
87    *   <li>Environment variable value of "GRPC_XDS_BOOTSTRAP_CONFIG"</li>
88    *   <li>Java System Property value of "io.grpc.xds.bootstrapConfig"</li>
89    * </ol>
90    */
91   @SuppressWarnings("unchecked")
92   @Override
bootstrap()93   public BootstrapInfo bootstrap() throws XdsInitializationException {
94     String filePath =
95         bootstrapPathFromEnvVar != null ? bootstrapPathFromEnvVar : bootstrapPathFromSysProp;
96     String fileContent;
97     if (filePath != null) {
98       logger.log(XdsLogLevel.INFO, "Reading bootstrap file from {0}", filePath);
99       try {
100         fileContent = reader.readFile(filePath);
101       } catch (IOException e) {
102         throw new XdsInitializationException("Fail to read bootstrap file", e);
103       }
104     } else {
105       fileContent = bootstrapConfigFromEnvVar != null
106           ? bootstrapConfigFromEnvVar : bootstrapConfigFromSysProp;
107     }
108     if (fileContent == null) {
109       throw new XdsInitializationException(
110           "Cannot find bootstrap configuration\n"
111               + "Environment variables searched:\n"
112               + "- " + BOOTSTRAP_PATH_SYS_ENV_VAR + "\n"
113               + "- " + BOOTSTRAP_CONFIG_SYS_ENV_VAR + "\n\n"
114               + "Java System Properties searched:\n"
115               + "- " + BOOTSTRAP_PATH_SYS_PROPERTY + "\n"
116               + "- " + BOOTSTRAP_CONFIG_SYS_PROPERTY + "\n\n");
117     }
118 
119     logger.log(XdsLogLevel.INFO, "Reading bootstrap from " + filePath);
120     Map<String, ?> rawBootstrap;
121     try {
122       rawBootstrap = (Map<String, ?>) JsonParser.parse(fileContent);
123     } catch (IOException e) {
124       throw new XdsInitializationException("Failed to parse JSON", e);
125     }
126     logger.log(XdsLogLevel.DEBUG, "Bootstrap configuration:\n{0}", rawBootstrap);
127     return bootstrap(rawBootstrap);
128   }
129 
130   @Override
bootstrap(Map<String, ?> rawData)131   BootstrapInfo bootstrap(Map<String, ?> rawData) throws XdsInitializationException {
132     BootstrapInfo.Builder builder = BootstrapInfo.builder();
133 
134     List<?> rawServerConfigs = JsonUtil.getList(rawData, "xds_servers");
135     if (rawServerConfigs == null) {
136       throw new XdsInitializationException("Invalid bootstrap: 'xds_servers' does not exist.");
137     }
138     List<ServerInfo> servers = parseServerInfos(rawServerConfigs, logger);
139     builder.servers(servers);
140 
141     Node.Builder nodeBuilder = Node.newBuilder();
142     Map<String, ?> rawNode = JsonUtil.getObject(rawData, "node");
143     if (rawNode != null) {
144       String id = JsonUtil.getString(rawNode, "id");
145       if (id != null) {
146         logger.log(XdsLogLevel.INFO, "Node id: {0}", id);
147         nodeBuilder.setId(id);
148       }
149       String cluster = JsonUtil.getString(rawNode, "cluster");
150       if (cluster != null) {
151         logger.log(XdsLogLevel.INFO, "Node cluster: {0}", cluster);
152         nodeBuilder.setCluster(cluster);
153       }
154       Map<String, ?> metadata = JsonUtil.getObject(rawNode, "metadata");
155       if (metadata != null) {
156         nodeBuilder.setMetadata(metadata);
157       }
158       Map<String, ?> rawLocality = JsonUtil.getObject(rawNode, "locality");
159       if (rawLocality != null) {
160         String region = "";
161         String zone = "";
162         String subZone = "";
163         if (rawLocality.containsKey("region")) {
164           region = JsonUtil.getString(rawLocality, "region");
165         }
166         if (rawLocality.containsKey("zone")) {
167           zone = JsonUtil.getString(rawLocality, "zone");
168         }
169         if (rawLocality.containsKey("sub_zone")) {
170           subZone = JsonUtil.getString(rawLocality, "sub_zone");
171         }
172         logger.log(XdsLogLevel.INFO, "Locality region: {0}, zone: {0}, subZone: {0}",
173             region, zone, subZone);
174         Locality locality = Locality.create(region, zone, subZone);
175         nodeBuilder.setLocality(locality);
176       }
177     }
178     GrpcBuildVersion buildVersion = GrpcUtil.getGrpcBuildVersion();
179     logger.log(XdsLogLevel.INFO, "Build version: {0}", buildVersion);
180     nodeBuilder.setBuildVersion(buildVersion.toString());
181     nodeBuilder.setUserAgentName(buildVersion.getUserAgent());
182     nodeBuilder.setUserAgentVersion(buildVersion.getImplementationVersion());
183     nodeBuilder.addClientFeatures(CLIENT_FEATURE_DISABLE_OVERPROVISIONING);
184     nodeBuilder.addClientFeatures(CLIENT_FEATURE_RESOURCE_IN_SOTW);
185     builder.node(nodeBuilder.build());
186 
187     Map<String, ?> certProvidersBlob = JsonUtil.getObject(rawData, "certificate_providers");
188     if (certProvidersBlob != null) {
189       logger.log(XdsLogLevel.INFO, "Configured with {0} cert providers", certProvidersBlob.size());
190       Map<String, CertificateProviderInfo> certProviders = new HashMap<>(certProvidersBlob.size());
191       for (String name : certProvidersBlob.keySet()) {
192         Map<String, ?> valueMap = JsonUtil.getObject(certProvidersBlob, name);
193         String pluginName =
194             checkForNull(JsonUtil.getString(valueMap, "plugin_name"), "plugin_name");
195         logger.log(XdsLogLevel.INFO, "cert provider: {0}, plugin name: {1}", name, pluginName);
196         Map<String, ?> config = checkForNull(JsonUtil.getObject(valueMap, "config"), "config");
197         CertificateProviderInfo certificateProviderInfo =
198             CertificateProviderInfo.create(pluginName, config);
199         certProviders.put(name, certificateProviderInfo);
200       }
201       builder.certProviders(certProviders);
202     }
203 
204     String grpcServerResourceId =
205         JsonUtil.getString(rawData, "server_listener_resource_name_template");
206     logger.log(
207         XdsLogLevel.INFO, "server_listener_resource_name_template: {0}", grpcServerResourceId);
208     builder.serverListenerResourceNameTemplate(grpcServerResourceId);
209 
210     if (!enableFederation) {
211       return builder.build();
212     }
213     String grpcClientDefaultListener =
214         JsonUtil.getString(rawData, "client_default_listener_resource_name_template");
215     logger.log(
216         XdsLogLevel.INFO, "client_default_listener_resource_name_template: {0}",
217         grpcClientDefaultListener);
218     if (grpcClientDefaultListener != null) {
219       builder.clientDefaultListenerResourceNameTemplate(grpcClientDefaultListener);
220     }
221 
222     Map<String, ?> rawAuthoritiesMap =
223         JsonUtil.getObject(rawData, "authorities");
224     ImmutableMap.Builder<String, AuthorityInfo> authorityInfoMapBuilder = ImmutableMap.builder();
225     if (rawAuthoritiesMap != null) {
226       logger.log(
227           XdsLogLevel.INFO, "Configured with {0} xDS server authorities", rawAuthoritiesMap.size());
228       for (String authorityName : rawAuthoritiesMap.keySet()) {
229         logger.log(XdsLogLevel.INFO, "xDS server authority: {0}", authorityName);
230         Map<String, ?> rawAuthority = JsonUtil.getObject(rawAuthoritiesMap, authorityName);
231         String clientListnerTemplate =
232             JsonUtil.getString(rawAuthority, "client_listener_resource_name_template");
233         logger.log(
234             XdsLogLevel.INFO, "client_listener_resource_name_template: {0}", clientListnerTemplate);
235         String prefix = XDSTP_SCHEME + "//" + authorityName + "/";
236         if (clientListnerTemplate == null) {
237           clientListnerTemplate = prefix + "envoy.config.listener.v3.Listener/%s";
238         } else if (!clientListnerTemplate.startsWith(prefix)) {
239           throw new XdsInitializationException(
240               "client_listener_resource_name_template: '" + clientListnerTemplate
241                   + "' does not start with " + prefix);
242         }
243         List<?> rawAuthorityServers = JsonUtil.getList(rawAuthority, "xds_servers");
244         List<ServerInfo> authorityServers;
245         if (rawAuthorityServers == null || rawAuthorityServers.isEmpty()) {
246           authorityServers = servers;
247         } else {
248           authorityServers = parseServerInfos(rawAuthorityServers, logger);
249         }
250         authorityInfoMapBuilder.put(
251             authorityName, AuthorityInfo.create(clientListnerTemplate, authorityServers));
252       }
253       builder.authorities(authorityInfoMapBuilder.buildOrThrow());
254     }
255 
256     return builder.build();
257   }
258 
parseServerInfos(List<?> rawServerConfigs, XdsLogger logger)259   private static List<ServerInfo> parseServerInfos(List<?> rawServerConfigs, XdsLogger logger)
260       throws XdsInitializationException {
261     logger.log(XdsLogLevel.INFO, "Configured with {0} xDS servers", rawServerConfigs.size());
262     ImmutableList.Builder<ServerInfo> servers = ImmutableList.builder();
263     List<Map<String, ?>> serverConfigList = JsonUtil.checkObjectList(rawServerConfigs);
264     for (Map<String, ?> serverConfig : serverConfigList) {
265       String serverUri = JsonUtil.getString(serverConfig, "server_uri");
266       if (serverUri == null) {
267         throw new XdsInitializationException("Invalid bootstrap: missing 'server_uri'");
268       }
269       logger.log(XdsLogLevel.INFO, "xDS server URI: {0}", serverUri);
270 
271       List<?> rawChannelCredsList = JsonUtil.getList(serverConfig, "channel_creds");
272       if (rawChannelCredsList == null || rawChannelCredsList.isEmpty()) {
273         throw new XdsInitializationException(
274             "Invalid bootstrap: server " + serverUri + " 'channel_creds' required");
275       }
276       ChannelCredentials channelCredentials =
277           parseChannelCredentials(JsonUtil.checkObjectList(rawChannelCredsList), serverUri);
278       if (channelCredentials == null) {
279         throw new XdsInitializationException(
280             "Server " + serverUri + ": no supported channel credentials found");
281       }
282 
283       boolean ignoreResourceDeletion = false;
284       List<String> serverFeatures = JsonUtil.getListOfStrings(serverConfig, "server_features");
285       if (serverFeatures != null) {
286         logger.log(XdsLogLevel.INFO, "Server features: {0}", serverFeatures);
287         ignoreResourceDeletion = serverFeatures.contains(SERVER_FEATURE_IGNORE_RESOURCE_DELETION);
288       }
289       servers.add(
290           ServerInfo.create(serverUri, channelCredentials, ignoreResourceDeletion));
291     }
292     return servers.build();
293   }
294 
295   @VisibleForTesting
setFileReader(FileReader reader)296   void setFileReader(FileReader reader) {
297     this.reader = reader;
298   }
299 
300   /**
301    * Reads the content of the file with the given path in the file system.
302    */
303   interface FileReader {
readFile(String path)304     String readFile(String path) throws IOException;
305   }
306 
307   private enum LocalFileReader implements FileReader {
308     INSTANCE;
309 
310     @Override
readFile(String path)311     public String readFile(String path) throws IOException {
312       return new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8);
313     }
314   }
315 
checkForNull(T value, String fieldName)316   private static <T> T checkForNull(T value, String fieldName) throws XdsInitializationException {
317     if (value == null) {
318       throw new XdsInitializationException(
319           "Invalid bootstrap: '" + fieldName + "' does not exist.");
320     }
321     return value;
322   }
323 
324   @Nullable
parseChannelCredentials(List<Map<String, ?>> jsonList, String serverUri)325   private static ChannelCredentials parseChannelCredentials(List<Map<String, ?>> jsonList,
326       String serverUri) throws XdsInitializationException {
327     for (Map<String, ?> channelCreds : jsonList) {
328       String type = JsonUtil.getString(channelCreds, "type");
329       if (type == null) {
330         throw new XdsInitializationException(
331             "Invalid bootstrap: server " + serverUri + " with 'channel_creds' type unspecified");
332       }
333       XdsCredentialsProvider provider =  XdsCredentialsRegistry.getDefaultRegistry()
334           .getProvider(type);
335       if (provider != null) {
336         Map<String, ?> config = JsonUtil.getObject(channelCreds, "config");
337         if (config == null) {
338           config = ImmutableMap.of();
339         }
340 
341         return provider.newChannelCredentials(config);
342       }
343     }
344     return null;
345   }
346 }
347