From 1f90e0e28d5628195cb1f861b73e45ed003f2973 Mon Sep 17 00:00:00 2001 From: ZHANG Dapeng Date: Mon, 18 Oct 2021 16:19:34 -0700 Subject: [PATCH] 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. --- .../main/java/io/grpc/xds/Bootstrapper.java | 93 +++++++++++- .../java/io/grpc/xds/BootstrapperImpl.java | 135 ++++++++++++------ .../io/grpc/xds/BootstrapperImplTest.java | 96 +++++++++++++ 3 files changed, 283 insertions(+), 41 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/Bootstrapper.java b/xds/src/main/java/io/grpc/xds/Bootstrapper.java index e1246701295..862f8691080 100644 --- a/xds/src/main/java/io/grpc/xds/Bootstrapper.java +++ b/xds/src/main/java/io/grpc/xds/Bootstrapper.java @@ -83,6 +83,38 @@ public static CertificateProviderInfo create(String pluginName, Map c } } + @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. + * + *

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). + * + *

Return value must start with {@code "xdstp:///"}. + */ + abstract String clientListenerResourceNameTemplate(); + + /** + * Ordered list of xDS servers to contact for this authority. + * + *

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). + * + *

If empty, the top-level server list {@link BootstrapInfo#servers()} will be used. + */ + abstract ImmutableList xdsServers(); + + static AuthorityInfo create( + String clientListenerResourceNameTemplate, List xdsServers) { + return new AutoValue_Bootstrapper_AuthorityInfo( + clientListenerResourceNameTemplate, ImmutableList.copyOf(xdsServers)); + } + } + /** * Data class containing the results of reading bootstrap. */ @@ -99,17 +131,71 @@ public abstract static class BootstrapInfo { @Nullable public abstract ImmutableMap certProviders(); + /** + * A template for the name of the Listener resource to subscribe to for a gRPC server. + * + *

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. + * + *

There is no default; if unset, xDS-based server creation fails. + */ @Nullable 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. + * + *

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 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. + * + *

Defaults to {@code "%s"}. + */ + abstract String clientDefaultListenerResourceNameTemplate(); + + /** + * A map of authority name to corresponding configuration. + * + *

This is used in the following cases: + * + *

+ * + *

In any of those cases, it is an error if the specified authority is not present in this + * map. + * + *

Defaults to an empty map. + */ + abstract ImmutableMap authorities(); + @VisibleForTesting static Builder builder() { - return new AutoValue_Bootstrapper_BootstrapInfo.Builder(); + return new AutoValue_Bootstrapper_BootstrapInfo.Builder() + .clientDefaultListenerResourceNameTemplate("%s") + .authorities(ImmutableMap.of()); } @AutoValue.Builder @VisibleForTesting abstract static class Builder { + abstract Builder servers(List servers); abstract Builder node(Node node); @@ -119,6 +205,11 @@ abstract static class Builder { abstract Builder serverListenerResourceNameTemplate( @Nullable String serverListenerResourceNameTemplate); + abstract Builder clientDefaultListenerResourceNameTemplate( + String clientDefaultListenerResourceNameTemplate); + + abstract Builder authorities(Map authorities); + abstract BootstrapInfo build(); } } diff --git a/xds/src/main/java/io/grpc/xds/BootstrapperImpl.java b/xds/src/main/java/io/grpc/xds/BootstrapperImpl.java index 0a044da5290..200ded3c1c4 100644 --- a/xds/src/main/java/io/grpc/xds/BootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/BootstrapperImpl.java @@ -17,6 +17,8 @@ package io.grpc.xds; 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.InsecureChannelCredentials; import io.grpc.Internal; @@ -33,7 +35,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -121,41 +122,14 @@ public BootstrapInfo bootstrap() throws XdsInitializationException { @Override BootstrapInfo bootstrap(Map rawData) throws XdsInitializationException { - List servers = new ArrayList<>(); + BootstrapInfo.Builder builder = BootstrapInfo.builder(); + List rawServerConfigs = JsonUtil.getList(rawData, "xds_servers"); if (rawServerConfigs == null) { throw new XdsInitializationException("Invalid bootstrap: 'xds_servers' does not exist."); } - logger.log(XdsLogLevel.INFO, "Configured with {0} xDS servers", rawServerConfigs.size()); - // TODO(chengyuanzhang): require at least one server URI. - List> serverConfigList = JsonUtil.checkObjectList(rawServerConfigs); - for (Map 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 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)); - } + List servers = parseServerInfos(rawServerConfigs, logger); + builder.servers(servers); Node.Builder nodeBuilder = Node.newBuilder(); Map rawNode = JsonUtil.getObject(rawData, "node"); @@ -200,29 +174,110 @@ BootstrapInfo bootstrap(Map rawData) throws XdsInitializationExceptio nodeBuilder.setUserAgentName(buildVersion.getUserAgent()); nodeBuilder.setUserAgentVersion(buildVersion.getImplementationVersion()); nodeBuilder.addClientFeatures(CLIENT_FEATURE_DISABLE_OVERPROVISIONING); + builder.node(nodeBuilder.build()); Map certProvidersBlob = JsonUtil.getObject(rawData, "certificate_providers"); - Map certProviders = null; if (certProvidersBlob != null) { - certProviders = new HashMap<>(certProvidersBlob.size()); + logger.log(XdsLogLevel.INFO, "Configured with {0} cert providers", certProvidersBlob.size()); + Map certProviders = new HashMap<>(certProvidersBlob.size()); for (String name : certProvidersBlob.keySet()) { Map valueMap = JsonUtil.getObject(certProvidersBlob, name); String pluginName = checkForNull(JsonUtil.getString(valueMap, "plugin_name"), "plugin_name"); + logger.log(XdsLogLevel.INFO, "cert provider: {0}, plugin name: {1}", name, pluginName); Map config = checkForNull(JsonUtil.getObject(valueMap, "config"), "config"); CertificateProviderInfo certificateProviderInfo = CertificateProviderInfo.create(pluginName, config); certProviders.put(name, certificateProviderInfo); } + builder.certProviders(certProviders); } + String grpcServerResourceId = JsonUtil.getString(rawData, "server_listener_resource_name_template"); - return BootstrapInfo.builder() - .servers(servers) - .node(nodeBuilder.build()) - .certProviders(certProviders) - .serverListenerResourceNameTemplate(grpcServerResourceId) - .build(); + logger.log( + XdsLogLevel.INFO, "server_listener_resource_name_template: {0}", grpcServerResourceId); + builder.serverListenerResourceNameTemplate(grpcServerResourceId); + + String grpcClientDefaultListener = + 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 rawAuthoritiesMap = + JsonUtil.getObject(rawData, "authorities"); + ImmutableMap.Builder 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 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 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 parseServerInfos(List rawServerConfigs, XdsLogger logger) + throws XdsInitializationException { + logger.log(XdsLogLevel.INFO, "Configured with {0} xDS servers", rawServerConfigs.size()); + ImmutableList.Builder servers = ImmutableList.builder(); + List> serverConfigList = JsonUtil.checkObjectList(rawServerConfigs); + for (Map 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 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 diff --git a/xds/src/test/java/io/grpc/xds/BootstrapperImplTest.java b/xds/src/test/java/io/grpc/xds/BootstrapperImplTest.java index 283efcea852..183d67018dc 100644 --- a/xds/src/test/java/io/grpc/xds/BootstrapperImplTest.java +++ b/xds/src/test/java/io/grpc/xds/BootstrapperImplTest.java @@ -27,6 +27,7 @@ import io.grpc.TlsChannelCredentials; import io.grpc.internal.GrpcUtil; import io.grpc.internal.GrpcUtil.GrpcBuildVersion; +import io.grpc.xds.Bootstrapper.AuthorityInfo; import io.grpc.xds.Bootstrapper.BootstrapInfo; import io.grpc.xds.Bootstrapper.ServerInfo; import io.grpc.xds.EnvoyProtoData.Node; @@ -677,6 +678,101 @@ public void fallbackToConfigFromSysProp() throws XdsInitializationException { 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://>/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( final String expectedPath, final String rawData) { return new BootstrapperImpl.FileReader() {