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