diff --git a/xds/src/main/java/io/grpc/xds/Bootstrapper.java b/xds/src/main/java/io/grpc/xds/Bootstrapper.java index 6fa3c4e571..c2a29ab10c 100644 --- a/xds/src/main/java/io/grpc/xds/Bootstrapper.java +++ b/xds/src/main/java/io/grpc/xds/Bootstrapper.java @@ -16,6 +16,8 @@ package io.grpc.xds; +import static com.google.common.base.Preconditions.checkNotNull; + import com.google.common.annotations.VisibleForTesting; import io.grpc.Internal; import io.grpc.internal.GrpcUtil; @@ -31,6 +33,7 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import javax.annotation.Nullable; @@ -160,7 +163,28 @@ public abstract class Bootstrapper { nodeBuilder.setUserAgentVersion(buildVersion.getImplementationVersion()); nodeBuilder.addClientFeatures(CLIENT_FEATURE_DISABLE_OVERPROVISIONING); - return new BootstrapInfo(servers, nodeBuilder.build()); + Map certProvidersBlob = JsonUtil.getObject(rawBootstrap, "certificate_providers"); + Map certProviders = null; + if (certProvidersBlob != null) { + 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"); + Map config = checkForNull(JsonUtil.getObject(valueMap, "config"), "config"); + CertificateProviderInfo certificateProviderInfo = + new CertificateProviderInfo(pluginName, config); + certProviders.put(name, certificateProviderInfo); + } + } + return new BootstrapInfo(servers, nodeBuilder.build(), certProviders); + } + + static T checkForNull(T value, String fieldName) throws IOException { + if (value == null) { + throw new IOException("Invalid bootstrap: '" + fieldName + "' does not exist."); + } + return value; } /** @@ -225,6 +249,30 @@ public abstract class Bootstrapper { } } + /** + * Data class containing Certificate provider information: the plugin-name and an opaque + * Map that represents the config for that plugin. + */ + @Internal + @Immutable + public static class CertificateProviderInfo { + private final String pluginName; + private final Map config; + + CertificateProviderInfo(String pluginName, Map config) { + this.pluginName = checkNotNull(pluginName, "pluginName"); + this.config = checkNotNull(config, "config"); + } + + String getPluginName() { + return pluginName; + } + + Map getConfig() { + return config; + } + } + /** * Data class containing the results of reading bootstrap. */ @@ -233,11 +281,14 @@ public abstract class Bootstrapper { public static class BootstrapInfo { private List servers; private final Node node; + @Nullable private final Map certProviders; @VisibleForTesting - BootstrapInfo(List servers, Node node) { + BootstrapInfo( + List servers, Node node, Map certProviders) { this.servers = servers; this.node = node; + this.certProviders = certProviders; } /** @@ -253,5 +304,10 @@ public abstract class Bootstrapper { public Node getNode() { return node; } + + /** Returns the cert-providers config map. */ + public Map getCertProviders() { + return Collections.unmodifiableMap(certProviders); + } } } diff --git a/xds/src/test/java/io/grpc/xds/BootstrapperTest.java b/xds/src/test/java/io/grpc/xds/BootstrapperTest.java index 6c1fd2111b..17f42aad73 100644 --- a/xds/src/test/java/io/grpc/xds/BootstrapperTest.java +++ b/xds/src/test/java/io/grpc/xds/BootstrapperTest.java @@ -17,6 +17,7 @@ package io.grpc.xds; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; @@ -28,6 +29,7 @@ import io.grpc.xds.EnvoyProtoData.Locality; import io.grpc.xds.EnvoyProtoData.Node; import java.io.IOException; import java.util.List; +import java.util.Map; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -298,6 +300,206 @@ public class BootstrapperTest { Bootstrapper.parseConfig(rawData); } + @Test + public void parseBootstrap_validData_certProviderInstances() throws IOException { + String rawData = + "{\n" + + " \"xds_servers\": [],\n" + + " \"certificate_providers\": {\n" + + " \"gcp_id\": {\n" + + " \"plugin_name\": \"meshca\",\n" + + " \"config\": {\n" + + " \"server\": {\n" + + " \"api_type\": \"GRPC\",\n" + + " \"grpc_services\": [{\n" + + " \"google_grpc\": {\n" + + " \"target_uri\": \"meshca.com\",\n" + + " \"channel_credentials\": {\"google_default\": {}},\n" + + " \"call_credentials\": [{\n" + + " \"sts_service\": {\n" + + " \"token_exchange_service\": \"securetoken.googleapis.com\",\n" + + " \"subject_token_path\": \"/etc/secret/sajwt.token\"\n" + + " }\n" + + " }]\n" // end call_credentials + + " },\n" // end google_grpc + + " \"time_out\": {\"seconds\": 10}\n" + + " }]\n" // end grpc_services + + " },\n" // end server + + " \"certificate_lifetime\": {\"seconds\": 86400},\n" + + " \"renewal_grace_period\": {\"seconds\": 3600},\n" + + " \"key_type\": \"RSA\",\n" + + " \"key_size\": 2048,\n" + + " \"location\": \"https://container.googleapis.com/v1/project/test-project1/locations/test-zone2/clusters/test-cluster3\"\n" + + " }\n" // end config + + " },\n" // end gcp_id + + " \"file_provider\": {\n" + + " \"plugin_name\": \"file_watcher\",\n" + + " \"config\": {\"path\": \"/etc/secret/certs\"}\n" + + " }\n" + + " }\n" + + "}"; + + BootstrapInfo info = Bootstrapper.parseConfig(rawData); + assertThat(info.getServers()).isEmpty(); + assertThat(info.getNode()).isEqualTo(getNodeBuilder().build()); + Map certProviders = info.getCertProviders(); + assertThat(certProviders).isNotNull(); + Bootstrapper.CertificateProviderInfo gcpId = certProviders.get("gcp_id"); + Bootstrapper.CertificateProviderInfo fileProvider = certProviders.get("file_provider"); + assertThat(gcpId.getPluginName()).isEqualTo("meshca"); + assertThat(gcpId.getConfig()).isInstanceOf(Map.class); + assertThat(fileProvider.getPluginName()).isEqualTo("file_watcher"); + assertThat(fileProvider.getConfig()).isInstanceOf(Map.class); + Map meshCaConfig = (Map)gcpId.getConfig(); + assertThat(meshCaConfig.get("key_size")).isEqualTo(2048); + } + + @Test + public void parseBootstrap_badPluginName() throws IOException { + String rawData = + "{\n" + + " \"xds_servers\": [],\n" + + " \"certificate_providers\": {\n" + + " \"gcp_id\": {\n" + + " \"plugin_name\": 234,\n" + + " \"config\": {\n" + + " \"server\": {\n" + + " \"api_type\": \"GRPC\",\n" + + " \"grpc_services\": [{\n" + + " \"google_grpc\": {\n" + + " \"target_uri\": \"meshca.com\",\n" + + " \"channel_credentials\": {\"google_default\": {}},\n" + + " \"call_credentials\": [{\n" + + " \"sts_service\": {\n" + + " \"token_exchange_service\": \"securetoken.googleapis.com\",\n" + + " \"subject_token_path\": \"/etc/secret/sajwt.token\"\n" + + " }\n" + + " }]\n" // end call_credentials + + " },\n" // end google_grpc + + " \"time_out\": {\"seconds\": 10}\n" + + " }]\n" // end grpc_services + + " },\n" // end server + + " \"certificate_lifetime\": {\"seconds\": 86400},\n" + + " \"renewal_grace_period\": {\"seconds\": 3600},\n" + + " \"key_type\": \"RSA\",\n" + + " \"key_size\": 2048,\n" + + " \"location\": \"https://container.googleapis.com/v1/project/test-project1/locations/test-zone2/clusters/test-cluster3\"\n" + + " }\n" // end config + + " },\n" // end gcp_id + + " \"file_provider\": {\n" + + " \"plugin_name\": \"file_watcher\",\n" + + " \"config\": {\"path\": \"/etc/secret/certs\"}\n" + + " }\n" + + " }\n" + + "}"; + + try { + Bootstrapper.parseConfig(rawData); + fail("exception expected"); + } catch (ClassCastException expected) { + assertThat(expected).hasMessageThat().contains("value '234.0' for key 'plugin_name' in"); + } + } + + @Test + public void parseBootstrap_badConfig() throws IOException { + String rawData = + "{\n" + + " \"xds_servers\": [],\n" + + " \"certificate_providers\": {\n" + + " \"gcp_id\": {\n" + + " \"plugin_name\": \"meshca\",\n" + + " \"config\": \"badValue\"\n" + + " },\n" // end gcp_id + + " \"file_provider\": {\n" + + " \"plugin_name\": \"file_watcher\",\n" + + " \"config\": {\"path\": \"/etc/secret/certs\"}\n" + + " }\n" + + " }\n" + + "}"; + + try { + Bootstrapper.parseConfig(rawData); + fail("exception expected"); + } catch (ClassCastException expected) { + assertThat(expected).hasMessageThat().contains("value 'badValue' for key 'config' in"); + } + } + + @Test + public void parseBootstrap_missingConfig() throws IOException { + String rawData = + "{\n" + + " \"xds_servers\": [],\n" + + " \"certificate_providers\": {\n" + + " \"gcp_id\": {\n" + + " \"plugin_name\": \"meshca\"\n" + + " },\n" // end gcp_id + + " \"file_provider\": {\n" + + " \"plugin_name\": \"file_watcher\",\n" + + " \"config\": {\"path\": \"/etc/secret/certs\"}\n" + + " }\n" + + " }\n" + + "}"; + + try { + Bootstrapper.parseConfig(rawData); + fail("exception expected"); + } catch (IOException expected) { + assertThat(expected) + .hasMessageThat() + .isEqualTo("Invalid bootstrap: 'config' does not exist."); + } + } + + @Test + public void parseBootstrap_missingPluginName() throws IOException { + String rawData = + "{\n" + + " \"xds_servers\": [],\n" + + " \"certificate_providers\": {\n" + + " \"gcp_id\": {\n" + + " \"plugin_name\": \"meshca\",\n" + + " \"config\": {\n" + + " \"server\": {\n" + + " \"api_type\": \"GRPC\",\n" + + " \"grpc_services\": [{\n" + + " \"google_grpc\": {\n" + + " \"target_uri\": \"meshca.com\",\n" + + " \"channel_credentials\": {\"google_default\": {}},\n" + + " \"call_credentials\": [{\n" + + " \"sts_service\": {\n" + + " \"token_exchange_service\": \"securetoken.googleapis.com\",\n" + + " \"subject_token_path\": \"/etc/secret/sajwt.token\"\n" + + " }\n" + + " }]\n" // end call_credentials + + " },\n" // end google_grpc + + " \"time_out\": {\"seconds\": 10}\n" + + " }]\n" // end grpc_services + + " },\n" // end server + + " \"certificate_lifetime\": {\"seconds\": 86400},\n" + + " \"renewal_grace_period\": {\"seconds\": 3600},\n" + + " \"key_type\": \"RSA\",\n" + + " \"key_size\": 2048,\n" + + " \"location\": \"https://container.googleapis.com/v1/project/test-project1/locations/test-zone2/clusters/test-cluster3\"\n" + + " }\n" // end config + + " },\n" // end gcp_id + + " \"file_provider\": {\n" + + " \"config\": {\"path\": \"/etc/secret/certs\"}\n" + + " }\n" + + " }\n" + + "}"; + + try { + Bootstrapper.parseConfig(rawData); + fail("exception expected"); + } catch (IOException expected) { + assertThat(expected) + .hasMessageThat() + .isEqualTo("Invalid bootstrap: 'plugin_name' does not exist."); + } + } + private static Node.Builder getNodeBuilder() { GrpcBuildVersion buildVersion = GrpcUtil.getGrpcBuildVersion(); return diff --git a/xds/src/test/java/io/grpc/xds/EdsLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/EdsLoadBalancerTest.java index 0b923b3033..62c72ba9e0 100644 --- a/xds/src/test/java/io/grpc/xds/EdsLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/EdsLoadBalancerTest.java @@ -232,7 +232,7 @@ public class EdsLoadBalancerTest { final List serverList = ImmutableList.of( new ServerInfo("trafficdirector.googleapis.com", ImmutableList.of(), null)); Node node = Node.newBuilder().build(); - BootstrapInfo bootstrapInfo = new BootstrapInfo(serverList, node); + BootstrapInfo bootstrapInfo = new BootstrapInfo(serverList, node, null); doReturn(bootstrapInfo).when(bootstrapper).readBootstrap(); if (isFullFlow) { diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverIntegrationTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverIntegrationTest.java index 7007f74ecb..3c844e08e9 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverIntegrationTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverIntegrationTest.java @@ -170,7 +170,7 @@ public class XdsNameResolverIntegrationTest { public BootstrapInfo readBootstrap() { List serverList = ImmutableList.of(new ServerInfo(serverName, ImmutableList.of(), null)); - return new BootstrapInfo(serverList, FAKE_BOOTSTRAP_NODE); + return new BootstrapInfo(serverList, FAKE_BOOTSTRAP_NODE, null); } }; xdsNameResolver = @@ -194,7 +194,7 @@ public class XdsNameResolverIntegrationTest { Bootstrapper bootstrapper = new Bootstrapper() { @Override public BootstrapInfo readBootstrap() { - return new BootstrapInfo(ImmutableList.of(), FAKE_BOOTSTRAP_NODE); + return new BootstrapInfo(ImmutableList.of(), FAKE_BOOTSTRAP_NODE, null); } };