diff --git a/xds/build.gradle b/xds/build.gradle index dd06273326..d587af0a33 100644 --- a/xds/build.gradle +++ b/xds/build.gradle @@ -20,11 +20,18 @@ dependencies { compile project(':grpc-protobuf'), project(':grpc-stub'), project(':grpc-core'), - project(':grpc-services') + project(':grpc-services'), + project(':grpc-auth') compile (libraries.protobuf_util) { // prefer 26.0-android from libraries instead of 20.0 exclude group: 'com.google.guava', module: 'guava' } + compile (libraries.google_auth_oauth2_http) { + // prefer 26.0-android from libraries instead of 25.1-android + exclude group: 'com.google.guava', module: 'guava' + // prefer 0.19.2 from libraries instead of 0.18.0 + exclude group: 'io.opencensus', module: 'opencensus-api' + } testCompile project(':grpc-core').sourceSets.test.output diff --git a/xds/src/main/java/io/grpc/xds/Bootstrapper.java b/xds/src/main/java/io/grpc/xds/Bootstrapper.java new file mode 100644 index 0000000000..e4416b386f --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/Bootstrapper.java @@ -0,0 +1,133 @@ +/* + * Copyright 2019 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import com.google.auth.oauth2.ComputeEngineCredentials; +import com.google.common.annotations.VisibleForTesting; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; +import io.envoyproxy.envoy.api.v2.core.ApiConfigSource; +import io.envoyproxy.envoy.api.v2.core.ApiConfigSource.ApiType; +import io.envoyproxy.envoy.api.v2.core.Node; +import io.grpc.CallCredentials; +import io.grpc.auth.MoreCallCredentials; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import javax.annotation.concurrent.Immutable; + +/** + * Loads configuration information to bootstrap xDS load balancer. + */ +@Immutable +abstract class Bootstrapper { + + private static final String BOOTSTRAP_PATH_SYS_ENV_VAR = "GRPC_XDS_BOOTSTRAP"; + + static Bootstrapper getInstance() throws Exception { + if (FileBasedBootstrapper.defaultInstance == null) { + throw FileBasedBootstrapper.failToBootstrapException; + } + return FileBasedBootstrapper.defaultInstance; + } + + /** + * Returns the canonical name of the traffic director to be connected to. + */ + abstract String getBalancerName(); + + /** + * Returns a {@link Node} message with project/network metadata in it to be included in + * xDS requests. + */ + abstract Node getNode(); + + /** + * Returns the credentials to use when communicating with the xDS server. + */ + abstract CallCredentials getCallCredentials(); + + @VisibleForTesting + static final class FileBasedBootstrapper extends Bootstrapper { + + private static final Exception failToBootstrapException; + private static final Bootstrapper defaultInstance; + + private final String balancerName; + private final Node node; + // TODO(chengyuanzhang): Add configuration for call credentials loaded from bootstrap file. + // hard-coded for alpha release. + + static { + Bootstrapper instance = null; + Exception exception = null; + try { + instance = new FileBasedBootstrapper(Bootstrapper.readConfig()); + } catch (Exception e) { + exception = e; + } + defaultInstance = instance; + failToBootstrapException = exception; + } + + @VisibleForTesting + FileBasedBootstrapper(Bootstrap bootstrapConfig) throws IOException { + ApiConfigSource serverConfig = bootstrapConfig.getXdsServer(); + if (!serverConfig.getApiType().equals(ApiType.GRPC)) { + throw new IOException("Unexpected api type: " + serverConfig.getApiType().toString()); + } + if (serverConfig.getGrpcServicesCount() != 1) { + throw new IOException( + "Unexpected number of gRPC services: expected: 1, actual: " + + serverConfig.getGrpcServicesCount()); + } + balancerName = serverConfig.getGrpcServices(0).getGoogleGrpc().getTargetUri(); + node = bootstrapConfig.getNode(); + } + + @Override + String getBalancerName() { + return balancerName; + } + + @Override + Node getNode() { + return node; + } + + @Override + CallCredentials getCallCredentials() { + return MoreCallCredentials.from(ComputeEngineCredentials.create()); + } + } + + private static Bootstrap readConfig() throws IOException { + String filePath = System.getenv(BOOTSTRAP_PATH_SYS_ENV_VAR); + if (filePath == null) { + throw new IOException("Environment variable " + BOOTSTRAP_PATH_SYS_ENV_VAR + " not found."); + } + return parseConfig(new String(Files.readAllBytes(Paths.get(filePath)), StandardCharsets.UTF_8)); + } + + @VisibleForTesting + static Bootstrap parseConfig(String rawData) throws InvalidProtocolBufferException { + Bootstrap.Builder bootstrapBuilder = Bootstrap.newBuilder(); + JsonFormat.parser().merge(rawData, bootstrapBuilder); + return bootstrapBuilder.build(); + } +} diff --git a/xds/src/main/proto/bootstrap.proto b/xds/src/main/proto/bootstrap.proto new file mode 100644 index 0000000000..6a45a56125 --- /dev/null +++ b/xds/src/main/proto/bootstrap.proto @@ -0,0 +1,39 @@ +// Copyright 2019 The gRPC Authors +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// An integration test service that covers all the method signature permutations +// of unary/streaming requests/responses. + +syntax = "proto3"; + +package io.grpc.xds; + +option java_outer_classname = "BootstrapProto"; +option java_multiple_files = true; +option java_package = "io.grpc.xds"; + +import "envoy/api/v2/core/base.proto"; +import "envoy/api/v2/core/config_source.proto"; + +// Configurations containing the information needed for xDS load balancer to bootstrap its +// communication with the xDS server. +// This proto message is defined for the convenience of parsing JSON bootstrap file in xDS load +// balancing policy only. It should not be used for any other purposes. +message Bootstrap { + // Metadata to be added to the Node message in xDS requests. + envoy.api.v2.core.Node node = 1 [json_name = "node"]; + + // Configurations including the name of the xDS server to contact, the credentials to use, etc. + envoy.api.v2.core.ApiConfigSource xds_server = 2 [json_name = "xds_server"]; +} diff --git a/xds/src/test/java/io/grpc/xds/BootstrapperTest.java b/xds/src/test/java/io/grpc/xds/BootstrapperTest.java new file mode 100644 index 0000000000..07158c9dee --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/BootstrapperTest.java @@ -0,0 +1,203 @@ +/* + * Copyright 2019 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Struct; +import com.google.protobuf.Value; +import io.envoyproxy.envoy.api.v2.core.ApiConfigSource; +import io.envoyproxy.envoy.api.v2.core.ApiConfigSource.ApiType; +import io.envoyproxy.envoy.api.v2.core.GrpcService; +import io.envoyproxy.envoy.api.v2.core.GrpcService.GoogleGrpc; +import io.envoyproxy.envoy.api.v2.core.Locality; +import io.envoyproxy.envoy.api.v2.core.Node; +import io.grpc.xds.Bootstrapper.FileBasedBootstrapper; +import java.io.IOException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link Bootstrapper}. */ +@RunWith(JUnit4.class) +public class BootstrapperTest { + + @Rule public ExpectedException thrown = ExpectedException.none(); + + @Test + public void validBootstrap() throws IOException { + Bootstrap config = + Bootstrap.newBuilder() + .setNode( + Node.newBuilder() + .setId("ENVOY_NODE_ID") + .setLocality( + Locality.newBuilder().setZone("ENVOY_ZONE").setRegion("ENVOY_REGION")) + .setMetadata( + Struct.newBuilder() + .putFields("TRAFFICDIRECTOR_INTERCEPTION_PORT", + Value.newBuilder().setStringValue("ENVOY_PORT").build()) + .putFields("TRAFFICDIRECTOR_NETWORK_NAME", + Value.newBuilder().setStringValue("VPC_NETWORK_NAME").build()))) + .setXdsServer(ApiConfigSource.newBuilder() + .setApiType(ApiType.GRPC) + .addGrpcServices( + GrpcService.newBuilder() + .setGoogleGrpc( + GoogleGrpc.newBuilder() + .setTargetUri("trafficdirector.googleapis.com:443").build()))) + .build(); + + Bootstrapper bootstrapper = new FileBasedBootstrapper(config); + assertThat(bootstrapper.getBalancerName()).isEqualTo("trafficdirector.googleapis.com:443"); + assertThat(bootstrapper.getNode()) + .isEqualTo( + Node.newBuilder() + .setId("ENVOY_NODE_ID") + .setLocality(Locality.newBuilder().setZone("ENVOY_ZONE").setRegion("ENVOY_REGION")) + .setMetadata( + Struct.newBuilder() + .putFields("TRAFFICDIRECTOR_INTERCEPTION_PORT", + Value.newBuilder().setStringValue("ENVOY_PORT").build()) + .putFields("TRAFFICDIRECTOR_NETWORK_NAME", + Value.newBuilder().setStringValue("VPC_NETWORK_NAME").build()) + .build()).build()); + } + + @Test + public void unsupportedApiType() throws IOException { + Bootstrap config = + Bootstrap.newBuilder() + .setNode( + Node.newBuilder() + .setId("ENVOY_NODE_ID") + .setLocality( + Locality.newBuilder().setZone("ENVOY_ZONE").setRegion("ENVOY_REGION")) + .setMetadata( + Struct.newBuilder() + .putFields("TRAFFICDIRECTOR_INTERCEPTION_PORT", + Value.newBuilder().setStringValue("ENVOY_PORT").build()) + .putFields("TRAFFICDIRECTOR_NETWORK_NAME", + Value.newBuilder().setStringValue("VPC_NETWORK_NAME").build()))) + .setXdsServer(ApiConfigSource.newBuilder() + .setApiType(ApiType.REST) + .addGrpcServices( + GrpcService.newBuilder() + .setGoogleGrpc( + GoogleGrpc.newBuilder() + .setTargetUri("trafficdirector.googleapis.com:443").build()))) + .build(); + + thrown.expect(IOException.class); + thrown.expectMessage("Unexpected api type: REST"); + new FileBasedBootstrapper(config); + } + + @Test + public void tooManyGrpcServices() throws IOException { + Bootstrap config = + Bootstrap.newBuilder() + .setNode( + Node.newBuilder() + .setId("ENVOY_NODE_ID") + .setLocality( + Locality.newBuilder().setZone("ENVOY_ZONE").setRegion("ENVOY_REGION")) + .setMetadata( + Struct.newBuilder() + .putFields("TRAFFICDIRECTOR_INTERCEPTION_PORT", + Value.newBuilder().setStringValue("ENVOY_PORT").build()) + .putFields("TRAFFICDIRECTOR_NETWORK_NAME", + Value.newBuilder().setStringValue("VPC_NETWORK_NAME").build()))) + .setXdsServer(ApiConfigSource.newBuilder() + .setApiType(ApiType.GRPC) + .addGrpcServices( + GrpcService.newBuilder() + .setGoogleGrpc( + GoogleGrpc.newBuilder() + .setTargetUri("trafficdirector.googleapis.com:443").build())) + .addGrpcServices( + GrpcService.newBuilder() + .setGoogleGrpc( + GoogleGrpc.newBuilder() + .setTargetUri("foobar.googleapis.com:443").build())) + ) + .build(); + + thrown.expect(IOException.class); + thrown.expectMessage("Unexpected number of gRPC services: expected: 1, actual: 2"); + new FileBasedBootstrapper(config); + } + + @Test + public void parseBootstrap_emptyData() throws InvalidProtocolBufferException { + String rawData = ""; + + thrown.expect(InvalidProtocolBufferException.class); + Bootstrapper.parseConfig(rawData); + } + + @Test + public void parseBootstrap_invalidNodeProto() throws InvalidProtocolBufferException { + String rawData = "{" + + "\"node\": {" + + "\"id\": \"ENVOY_NODE_ID\"," + + "\"bad_field\": \"bad_value\"" + + "\"locality\": {" + + "\"zone\": \"ENVOY_ZONE\"}," + + "\"metadata\": {" + + "\"TRAFFICDIRECTOR_INTERCEPTION_PORT\": \"ENVOY_PORT\", " + + "\"TRAFFICDIRECTOR_NETWORK_NAME\": \"VPC_NETWORK_NAME\"" + + "}" + + "}," + + "\"xds_server\": {" + + "\"api_type\": \"GRPC\"," + + "\"grpc_services\": " + + "[ {\"google_grpc\": {\"target_uri\": \"trafficdirector.googleapis.com:443\"} } ]" + + "} " + + "}"; + + thrown.expect(InvalidProtocolBufferException.class); + Bootstrapper.parseConfig(rawData); + } + + @Test + public void parseBootstrap_invalidApiConfigSourceProto() throws InvalidProtocolBufferException { + String rawData = "{" + + "\"node\": {" + + "\"id\": \"ENVOY_NODE_ID\"," + + "\"locality\": {" + + "\"zone\": \"ENVOY_ZONE\"}," + + "\"metadata\": {" + + "\"TRAFFICDIRECTOR_INTERCEPTION_PORT\": \"ENVOY_PORT\", " + + "\"TRAFFICDIRECTOR_NETWORK_NAME\": \"VPC_NETWORK_NAME\"" + + "}" + + "}," + + "\"xds_server\": {" + + "\"api_type\": \"GRPC\"," + + "\"bad_field\": \"bad_value\"" + + "\"grpc_services\": " + + "[ {\"google_grpc\": {\"target_uri\": \"trafficdirector.googleapis.com:443\"} } ]" + + "} " + + "}"; + + thrown.expect(InvalidProtocolBufferException.class); + Bootstrapper.parseConfig(rawData); + } +}