xds: implement bootstrapping for xDS load balancer for alpha release (#6145)

* Defined a proto message that maps the bootstrap JSON file for convenient parsing purpose.

* Implemented a Bootstrapper class which reads a local bootstrap file.

* Added unit test for Bootstrapper.

* Let xDS load balancer bootstrap from a bootstrap file. Currently not use configurations from bootstrap in xDS communication as the xDS load balancer implementation is undergoing changes. We will integrate it later.

* Added newline at the end of files.

* Added json_name proto field option.

* Replaced all RuntimeException with IOException for bootstrap data errors.

* Changed to use JUnit exception rules in test.

* Use StandardCharSets.UTF_8

* Revert "Let xDS load balancer bootstrap from a bootstrap file. Currently not use configurations from bootstrap in xDS communication as the xDS load balancer implementation is undergoing changes. We will integrate it later."

This reverts commit 37200cdd3c.

* Use initialization-on-demand holder idiom for instantiating Bootstrapper instance.

* Fixed usage of JUnit exception rules.

* Changed lazily instantiated variable to camel case

* Removed unnecessary constructor.
This commit is contained in:
Chengyuan Zhang 2019-09-22 16:49:09 -07:00 committed by GitHub
parent fe77496f76
commit 65321b5a67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 383 additions and 1 deletions

View File

@ -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

View File

@ -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();
}
}

View File

@ -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"];
}

View File

@ -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);
}
}