xds: add and parse new bootstrap fields for federation (#8608)

Made changes as per "Bootstrap File Changes" section in go/grpc-xds-federation and implemented bootstrap file parsing logic for the change.
This commit is contained in:
ZHANG Dapeng 2021-10-18 16:19:34 -07:00 committed by GitHub
parent e9b0c2e851
commit 1f90e0e28d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 283 additions and 41 deletions

View File

@ -83,6 +83,38 @@ public abstract class Bootstrapper {
} }
} }
@AutoValue
abstract static class AuthorityInfo {
/**
* A template for the name of the Listener resource to subscribe to for a gRPC client
* channel. Used only when the channel is created using an "xds:" URI with this authority
* name.
*
* <p>The token "%s", if present in this string, will be replaced with %-encoded
* service authority (i.e., the path part of the target URI used to create the gRPC channel).
*
* <p>Return value must start with {@code "xdstp://<authority_name>/"}.
*/
abstract String clientListenerResourceNameTemplate();
/**
* Ordered list of xDS servers to contact for this authority.
*
* <p>If the same server is listed in multiple authorities, the entries will be de-duped (i.e.,
* resources for both authorities will be fetched on the same ADS stream).
*
* <p>If empty, the top-level server list {@link BootstrapInfo#servers()} will be used.
*/
abstract ImmutableList<ServerInfo> xdsServers();
static AuthorityInfo create(
String clientListenerResourceNameTemplate, List<ServerInfo> xdsServers) {
return new AutoValue_Bootstrapper_AuthorityInfo(
clientListenerResourceNameTemplate, ImmutableList.copyOf(xdsServers));
}
}
/** /**
* Data class containing the results of reading bootstrap. * Data class containing the results of reading bootstrap.
*/ */
@ -99,17 +131,71 @@ public abstract class Bootstrapper {
@Nullable @Nullable
public abstract ImmutableMap<String, CertificateProviderInfo> certProviders(); public abstract ImmutableMap<String, CertificateProviderInfo> certProviders();
/**
* A template for the name of the Listener resource to subscribe to for a gRPC server.
*
* <p>If starts with "xdstp:", will be interpreted as a new-style name, in which case the
* authority of the URI will be used to select the relevant configuration in the
* "authorities" map. The token "%s", if present in this string, will be replaced with
* the IP and port on which the server is listening. If the template starts with "xdstp:",
* the replaced string will be %-encoded.
*
* <p>There is no default; if unset, xDS-based server creation fails.
*/
@Nullable @Nullable
public abstract String serverListenerResourceNameTemplate(); public abstract String serverListenerResourceNameTemplate();
/**
* A template for the name of the Listener resource to subscribe to for a gRPC client channel.
* Used only when the channel is created with an "xds:" URI with no authority.
*
* <p>If starts with "xdstp:", will be interpreted as a new-style name, in which case the
* authority of the URI will be used to select the relevant configuration in the "authorities"
* map.
*
* <p>The token "%s", if present in this string, will be replaced with the service authority
* (i.e., the path part of the target URI used to create the gRPC channel). If the template
* starts with "xdstp:", the replaced string will be %-encoded.
*
* <p>Defaults to {@code "%s"}.
*/
abstract String clientDefaultListenerResourceNameTemplate();
/**
* A map of authority name to corresponding configuration.
*
* <p>This is used in the following cases:
*
* <ul>
* <li>A gRPC client channel is created using an "xds:" URI that includes an
* authority.</li>
*
* <li>A gRPC client channel is created using an "xds:" URI with no authority,
* but the "client_default_listener_resource_name_template" field above turns it into an
* "xdstp:" URI.</li>
*
* <li>A gRPC server is created and the "server_listener_resource_name_template" field is an
* "xdstp:" URI.</li>
* </ul>
*
* <p>In any of those cases, it is an error if the specified authority is not present in this
* map.
*
* <p>Defaults to an empty map.
*/
abstract ImmutableMap<String, AuthorityInfo> authorities();
@VisibleForTesting @VisibleForTesting
static Builder builder() { static Builder builder() {
return new AutoValue_Bootstrapper_BootstrapInfo.Builder(); return new AutoValue_Bootstrapper_BootstrapInfo.Builder()
.clientDefaultListenerResourceNameTemplate("%s")
.authorities(ImmutableMap.<String, AuthorityInfo>of());
} }
@AutoValue.Builder @AutoValue.Builder
@VisibleForTesting @VisibleForTesting
abstract static class Builder { abstract static class Builder {
abstract Builder servers(List<ServerInfo> servers); abstract Builder servers(List<ServerInfo> servers);
abstract Builder node(Node node); abstract Builder node(Node node);
@ -119,6 +205,11 @@ public abstract class Bootstrapper {
abstract Builder serverListenerResourceNameTemplate( abstract Builder serverListenerResourceNameTemplate(
@Nullable String serverListenerResourceNameTemplate); @Nullable String serverListenerResourceNameTemplate);
abstract Builder clientDefaultListenerResourceNameTemplate(
String clientDefaultListenerResourceNameTemplate);
abstract Builder authorities(Map<String, AuthorityInfo> authorities);
abstract BootstrapInfo build(); abstract BootstrapInfo build();
} }
} }

View File

@ -17,6 +17,8 @@
package io.grpc.xds; package io.grpc.xds;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import io.grpc.ChannelCredentials; import io.grpc.ChannelCredentials;
import io.grpc.InsecureChannelCredentials; import io.grpc.InsecureChannelCredentials;
import io.grpc.Internal; import io.grpc.Internal;
@ -33,7 +35,6 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -121,41 +122,14 @@ public class BootstrapperImpl extends Bootstrapper {
@Override @Override
BootstrapInfo bootstrap(Map<String, ?> rawData) throws XdsInitializationException { BootstrapInfo bootstrap(Map<String, ?> rawData) throws XdsInitializationException {
List<ServerInfo> servers = new ArrayList<>(); BootstrapInfo.Builder builder = BootstrapInfo.builder();
List<?> rawServerConfigs = JsonUtil.getList(rawData, "xds_servers"); List<?> rawServerConfigs = JsonUtil.getList(rawData, "xds_servers");
if (rawServerConfigs == null) { if (rawServerConfigs == null) {
throw new XdsInitializationException("Invalid bootstrap: 'xds_servers' does not exist."); throw new XdsInitializationException("Invalid bootstrap: 'xds_servers' does not exist.");
} }
logger.log(XdsLogLevel.INFO, "Configured with {0} xDS servers", rawServerConfigs.size()); List<ServerInfo> servers = parseServerInfos(rawServerConfigs, logger);
// TODO(chengyuanzhang): require at least one server URI. builder.servers(servers);
List<Map<String, ?>> serverConfigList = JsonUtil.checkObjectList(rawServerConfigs);
for (Map<String, ?> serverConfig : serverConfigList) {
String serverUri = JsonUtil.getString(serverConfig, "server_uri");
if (serverUri == null) {
throw new XdsInitializationException("Invalid bootstrap: missing 'server_uri'");
}
logger.log(XdsLogLevel.INFO, "xDS server URI: {0}", serverUri);
List<?> rawChannelCredsList = JsonUtil.getList(serverConfig, "channel_creds");
if (rawChannelCredsList == null || rawChannelCredsList.isEmpty()) {
throw new XdsInitializationException(
"Invalid bootstrap: server " + serverUri + " 'channel_creds' required");
}
ChannelCredentials channelCredentials =
parseChannelCredentials(JsonUtil.checkObjectList(rawChannelCredsList), serverUri);
if (channelCredentials == null) {
throw new XdsInitializationException(
"Server " + serverUri + ": no supported channel credentials found");
}
boolean useProtocolV3 = false;
List<String> serverFeatures = JsonUtil.getListOfStrings(serverConfig, "server_features");
if (serverFeatures != null) {
logger.log(XdsLogLevel.INFO, "Server features: {0}", serverFeatures);
useProtocolV3 = serverFeatures.contains(XDS_V3_SERVER_FEATURE);
}
servers.add(ServerInfo.create(serverUri, channelCredentials, useProtocolV3));
}
Node.Builder nodeBuilder = Node.newBuilder(); Node.Builder nodeBuilder = Node.newBuilder();
Map<String, ?> rawNode = JsonUtil.getObject(rawData, "node"); Map<String, ?> rawNode = JsonUtil.getObject(rawData, "node");
@ -200,29 +174,110 @@ public class BootstrapperImpl extends Bootstrapper {
nodeBuilder.setUserAgentName(buildVersion.getUserAgent()); nodeBuilder.setUserAgentName(buildVersion.getUserAgent());
nodeBuilder.setUserAgentVersion(buildVersion.getImplementationVersion()); nodeBuilder.setUserAgentVersion(buildVersion.getImplementationVersion());
nodeBuilder.addClientFeatures(CLIENT_FEATURE_DISABLE_OVERPROVISIONING); nodeBuilder.addClientFeatures(CLIENT_FEATURE_DISABLE_OVERPROVISIONING);
builder.node(nodeBuilder.build());
Map<String, ?> certProvidersBlob = JsonUtil.getObject(rawData, "certificate_providers"); Map<String, ?> certProvidersBlob = JsonUtil.getObject(rawData, "certificate_providers");
Map<String, CertificateProviderInfo> certProviders = null;
if (certProvidersBlob != null) { if (certProvidersBlob != null) {
certProviders = new HashMap<>(certProvidersBlob.size()); logger.log(XdsLogLevel.INFO, "Configured with {0} cert providers", certProvidersBlob.size());
Map<String, CertificateProviderInfo> certProviders = new HashMap<>(certProvidersBlob.size());
for (String name : certProvidersBlob.keySet()) { for (String name : certProvidersBlob.keySet()) {
Map<String, ?> valueMap = JsonUtil.getObject(certProvidersBlob, name); Map<String, ?> valueMap = JsonUtil.getObject(certProvidersBlob, name);
String pluginName = String pluginName =
checkForNull(JsonUtil.getString(valueMap, "plugin_name"), "plugin_name"); checkForNull(JsonUtil.getString(valueMap, "plugin_name"), "plugin_name");
logger.log(XdsLogLevel.INFO, "cert provider: {0}, plugin name: {1}", name, pluginName);
Map<String, ?> config = checkForNull(JsonUtil.getObject(valueMap, "config"), "config"); Map<String, ?> config = checkForNull(JsonUtil.getObject(valueMap, "config"), "config");
CertificateProviderInfo certificateProviderInfo = CertificateProviderInfo certificateProviderInfo =
CertificateProviderInfo.create(pluginName, config); CertificateProviderInfo.create(pluginName, config);
certProviders.put(name, certificateProviderInfo); certProviders.put(name, certificateProviderInfo);
} }
builder.certProviders(certProviders);
} }
String grpcServerResourceId = String grpcServerResourceId =
JsonUtil.getString(rawData, "server_listener_resource_name_template"); JsonUtil.getString(rawData, "server_listener_resource_name_template");
return BootstrapInfo.builder() logger.log(
.servers(servers) XdsLogLevel.INFO, "server_listener_resource_name_template: {0}", grpcServerResourceId);
.node(nodeBuilder.build()) builder.serverListenerResourceNameTemplate(grpcServerResourceId);
.certProviders(certProviders)
.serverListenerResourceNameTemplate(grpcServerResourceId) String grpcClientDefaultListener =
.build(); JsonUtil.getString(rawData, "client_default_listener_resource_name_template");
logger.log(
XdsLogLevel.INFO, "client_default_listener_resource_name_template: {0}",
grpcClientDefaultListener);
if (grpcClientDefaultListener != null) {
builder.clientDefaultListenerResourceNameTemplate(grpcClientDefaultListener);
}
Map<String, ?> rawAuthoritiesMap =
JsonUtil.getObject(rawData, "authorities");
ImmutableMap.Builder<String, AuthorityInfo> authorityInfoMapBuilder = ImmutableMap.builder();
if (rawAuthoritiesMap != null) {
logger.log(
XdsLogLevel.INFO, "Configured with {0} xDS server authorities", rawAuthoritiesMap.size());
for (String authorityName : rawAuthoritiesMap.keySet()) {
logger.log(XdsLogLevel.INFO, "xDS server authority: {0}", authorityName);
Map<String, ?> rawAuthority = JsonUtil.getObject(rawAuthoritiesMap, authorityName);
String clientListnerTemplate =
JsonUtil.getString(rawAuthority, "client_listener_resource_name_template");
logger.log(
XdsLogLevel.INFO, "client_listener_resource_name_template: {0}", clientListnerTemplate);
String prefix = "xdstp://" + authorityName + "/";
if (clientListnerTemplate == null) {
clientListnerTemplate = prefix + "envoy.config.listener.v3.Listener/%s";
} else if (!clientListnerTemplate.startsWith(prefix)) {
throw new XdsInitializationException(
"client_listener_resource_name_template: '" + clientListnerTemplate
+ "' does not start with " + prefix);
}
List<?> rawAuthorityServers = JsonUtil.getList(rawAuthority, "xds_servers");
List<ServerInfo> authorityServers;
if (rawAuthorityServers == null || rawAuthorityServers.isEmpty()) {
authorityServers = servers;
} else {
authorityServers = parseServerInfos(rawAuthorityServers, logger);
}
authorityInfoMapBuilder.put(
authorityName, AuthorityInfo.create(clientListnerTemplate, authorityServers));
}
builder.authorities(authorityInfoMapBuilder.build());
}
return builder.build();
}
private static List<ServerInfo> parseServerInfos(List<?> rawServerConfigs, XdsLogger logger)
throws XdsInitializationException {
logger.log(XdsLogLevel.INFO, "Configured with {0} xDS servers", rawServerConfigs.size());
ImmutableList.Builder<ServerInfo> servers = ImmutableList.builder();
List<Map<String, ?>> serverConfigList = JsonUtil.checkObjectList(rawServerConfigs);
for (Map<String, ?> serverConfig : serverConfigList) {
String serverUri = JsonUtil.getString(serverConfig, "server_uri");
if (serverUri == null) {
throw new XdsInitializationException("Invalid bootstrap: missing 'server_uri'");
}
logger.log(XdsLogLevel.INFO, "xDS server URI: {0}", serverUri);
List<?> rawChannelCredsList = JsonUtil.getList(serverConfig, "channel_creds");
if (rawChannelCredsList == null || rawChannelCredsList.isEmpty()) {
throw new XdsInitializationException(
"Invalid bootstrap: server " + serverUri + " 'channel_creds' required");
}
ChannelCredentials channelCredentials =
parseChannelCredentials(JsonUtil.checkObjectList(rawChannelCredsList), serverUri);
if (channelCredentials == null) {
throw new XdsInitializationException(
"Server " + serverUri + ": no supported channel credentials found");
}
boolean useProtocolV3 = false;
List<String> serverFeatures = JsonUtil.getListOfStrings(serverConfig, "server_features");
if (serverFeatures != null) {
logger.log(XdsLogLevel.INFO, "Server features: {0}", serverFeatures);
useProtocolV3 = serverFeatures.contains(XDS_V3_SERVER_FEATURE);
}
servers.add(ServerInfo.create(serverUri, channelCredentials, useProtocolV3));
}
return servers.build();
} }
@VisibleForTesting @VisibleForTesting

View File

@ -27,6 +27,7 @@ import io.grpc.InsecureChannelCredentials;
import io.grpc.TlsChannelCredentials; import io.grpc.TlsChannelCredentials;
import io.grpc.internal.GrpcUtil; import io.grpc.internal.GrpcUtil;
import io.grpc.internal.GrpcUtil.GrpcBuildVersion; import io.grpc.internal.GrpcUtil.GrpcBuildVersion;
import io.grpc.xds.Bootstrapper.AuthorityInfo;
import io.grpc.xds.Bootstrapper.BootstrapInfo; import io.grpc.xds.Bootstrapper.BootstrapInfo;
import io.grpc.xds.Bootstrapper.ServerInfo; import io.grpc.xds.Bootstrapper.ServerInfo;
import io.grpc.xds.EnvoyProtoData.Node; import io.grpc.xds.EnvoyProtoData.Node;
@ -677,6 +678,101 @@ public class BootstrapperImplTest {
bootstrapper.bootstrap(); bootstrapper.bootstrap();
} }
@Test
public void parseClientDefaultListenerResourceNameTemplate() throws Exception {
String rawData = "{\n"
+ " \"xds_servers\": [\n"
+ " ]\n"
+ "}";
bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData));
BootstrapInfo info = bootstrapper.bootstrap();
assertThat(info.clientDefaultListenerResourceNameTemplate()).isEqualTo("%s");
rawData = "{\n"
+ " \"client_default_listener_resource_name_template\": \"xdstp://a.com/faketype/%s\",\n"
+ " \"xds_servers\": [\n"
+ " ]\n"
+ "}";
bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData));
info = bootstrapper.bootstrap();
assertThat(info.clientDefaultListenerResourceNameTemplate())
.isEqualTo("xdstp://a.com/faketype/%s");
}
@Test
public void parseAuthorities() throws Exception {
String rawData = "{\n"
+ " \"xds_servers\": [\n"
+ " {\n"
+ " \"server_uri\": \"" + SERVER_URI + "\",\n"
+ " \"channel_creds\": [\n"
+ " {\"type\": \"insecure\"}\n"
+ " ]\n"
+ " }\n"
+ " ]\n"
+ "}";
bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData));
BootstrapInfo info = bootstrapper.bootstrap();
assertThat(info.authorities()).isEmpty();
rawData = "{\n"
+ " \"authorities\": {\n"
+ " \"a.com\": {\n"
+ " \"client_listener_resource_name_template\": \"xdstp://a.com/v1.Listener/id-%s\"\n"
+ " }\n"
+ " },\n"
+ " \"xds_servers\": [\n"
+ " {\n"
+ " \"server_uri\": \"" + SERVER_URI + "\",\n"
+ " \"channel_creds\": [\n"
+ " {\"type\": \"insecure\"}\n"
+ " ]\n"
+ " }\n"
+ " ]\n"
+ "}";
bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData));
info = bootstrapper.bootstrap();
assertThat(info.authorities()).hasSize(1);
AuthorityInfo authorityInfo = info.authorities().get("a.com");
assertThat(authorityInfo.clientListenerResourceNameTemplate())
.isEqualTo("xdstp://a.com/v1.Listener/id-%s");
// Defaults to top-level servers.
assertThat(authorityInfo.xdsServers()).hasSize(1);
assertThat(authorityInfo.xdsServers().get(0).target()).isEqualTo(SERVER_URI);
rawData = "{\n"
+ " \"authorities\": {\n"
+ " \"a.com\": {\n"
+ " \"xds_servers\": [\n"
+ " {\n"
+ " \"server_uri\": \"td2.googleapis.com:443\",\n"
+ " \"channel_creds\": [\n"
+ " {\"type\": \"insecure\"}\n"
+ " ]\n"
+ " }\n"
+ " ]\n"
+ " }\n"
+ " },\n"
+ " \"xds_servers\": [\n"
+ " {\n"
+ " \"server_uri\": \"" + SERVER_URI + "\",\n"
+ " \"channel_creds\": [\n"
+ " {\"type\": \"insecure\"}\n"
+ " ]\n"
+ " }\n"
+ " ]\n"
+ "}";
bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData));
info = bootstrapper.bootstrap();
assertThat(info.authorities()).hasSize(1);
authorityInfo = info.authorities().get("a.com");
// Defaults to "xdstp://<authority_name>>/envoy.config.listener.v3.Listener/%s"
assertThat(authorityInfo.clientListenerResourceNameTemplate())
.isEqualTo("xdstp://a.com/envoy.config.listener.v3.Listener/%s");
assertThat(authorityInfo.xdsServers()).hasSize(1);
assertThat(authorityInfo.xdsServers().get(0).target()).isEqualTo("td2.googleapis.com:443");
}
private static BootstrapperImpl.FileReader createFileReader( private static BootstrapperImpl.FileReader createFileReader(
final String expectedPath, final String rawData) { final String expectedPath, final String rawData) {
return new BootstrapperImpl.FileReader() { return new BootstrapperImpl.FileReader() {