Skip to content

Commit

Permalink
grpc: Consolidate gRPC code from BES and Remote Execution. Fixes #3460,
Browse files Browse the repository at this point in the history
#3486

BES and Remote Execution have separate implementations of gRPC channel
creation, authentication and TLS. We should merge them, to avoid
duplication and bugs. One such bug is #3640, where the BES code had a
different implementation for Google Application Default Credentials.

RELNOTES: The Build Event Service (BES) client now properly supports
Google Applicaton Default Credentials.
PiperOrigin-RevId: 164253879
  • Loading branch information
buchgr committed Aug 11, 2017
1 parent 8e1c776 commit 37bd5c0
Show file tree
Hide file tree
Showing 11 changed files with 126 additions and 239 deletions.
9 changes: 8 additions & 1 deletion src/main/java/com/google/devtools/build/lib/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ java_library(

java_library(
name = "auth_and_tls_options",
srcs = ["authandtls/AuthAndTLSOptions.java"],
srcs = glob(["authandtls/*.java"]),
visibility = [
"//src/main/java/com/google/devtools/build/lib/remote:__pkg__",
"//src/test/java/com/google/devtools/build/lib:__pkg__",
Expand All @@ -401,6 +401,13 @@ java_library(
],
deps = [
"//src/main/java/com/google/devtools/common/options",
"//third_party:apache_httpclient",
"//third_party:apache_httpcore",
"//third_party:auth",
"//third_party:guava",
"//third_party:jsr305",
"//third_party:netty",
"//third_party/grpc:grpc-jar",
],
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public class AuthAndTLSOptions extends OptionsBase {

@Option(
name = "auth_scope",
defaultValue = "null",
defaultValue = "https://www.googleapis.com/auth/cloud-build-service",
category = "remote",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.devtools.build.lib.remote;
package com.google.devtools.build.lib.authandtls;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.authandtls.AuthAndTLSOptions;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import io.grpc.CallCredentials;
import io.grpc.ManagedChannel;
import io.grpc.auth.MoreCallCredentials;
import io.grpc.netty.GrpcSslContexts;
import io.grpc.netty.NegotiationType;
import io.grpc.netty.NettyChannelBuilder;
import io.grpc.util.RoundRobinLoadBalancerFactory;
import io.netty.handler.ssl.SslContext;
import java.io.File;
import java.io.FileInputStream;
Expand All @@ -30,70 +33,91 @@
import java.io.InputStream;
import javax.annotation.Nullable;

/** Instantiate all authentication helpers from build options. */
@ThreadSafe
public final class ChannelOptions {
private final boolean tlsEnabled;
private final SslContext sslContext;
private final String tlsAuthorityOverride;
private final CallCredentials credentials;
/**
* Utility methods for using {@link AuthAndTLSOptions} with gRPC.
*/
public final class GrpcUtils {

private ChannelOptions(
boolean tlsEnabled,
SslContext sslContext,
String tlsAuthorityOverride,
CallCredentials credentials) {
this.tlsEnabled = tlsEnabled;
this.sslContext = sslContext;
this.tlsAuthorityOverride = tlsAuthorityOverride;
this.credentials = credentials;
}
/**
* Create a new gRPC {@link ManagedChannel}.
*
* @throws IOException in case the channel can't be constructed.
*/
public static ManagedChannel newChannel(String target, AuthAndTLSOptions options)
throws IOException {
Preconditions.checkNotNull(target);
Preconditions.checkNotNull(options);

public boolean tlsEnabled() {
return tlsEnabled;
}
final SslContext sslContext =
options.tlsEnabled ? createSSlContext(options.tlsCertificate) : null;

public CallCredentials getCallCredentials() {
return credentials;
try {
NettyChannelBuilder builder =
NettyChannelBuilder.forTarget(target)
.negotiationType(
options.tlsEnabled ? NegotiationType.TLS : NegotiationType.PLAINTEXT)
.loadBalancerFactory(RoundRobinLoadBalancerFactory.getInstance());
if (sslContext != null) {
builder.sslContext(sslContext);
if (options.tlsAuthorityOverride != null) {
builder.overrideAuthority(options.tlsAuthorityOverride);
}
}
return builder.build();
} catch (RuntimeException e) {
// gRPC might throw all kinds of RuntimeExceptions: StatusRuntimeException,
// IllegalStateException, NullPointerException, ...
String message = "Failed to connect to '%s': %s";
throw new IOException(String.format(message, target, e.getMessage()));
}
}

public String getTlsAuthorityOverride() {
return tlsAuthorityOverride;
private static SslContext createSSlContext(@Nullable String rootCert) throws IOException {
if (rootCert == null) {
try {
return GrpcSslContexts.forClient().build();
} catch (Exception e) {
String message = "Failed to init TLS infrastructure: "
+ e.getMessage();
throw new IOException(message, e);
}
} else {
try {
return GrpcSslContexts.forClient().trustManager(new File(rootCert)).build();
} catch (Exception e) {
String message = "Failed to init TLS infrastructure using '%s' as root certificate: %s";
message = String.format(message, rootCert, e.getMessage());
throw new IOException(message, e);
}
}
}

public SslContext getSslContext() {
return sslContext;
}
/**
* Create a new {@link CallCredentials} object.
*
* @throws IOException in case the call credentials can't be constructed.
*/
public static CallCredentials newCallCredentials(AuthAndTLSOptions options) throws IOException {
if (!options.authEnabled) {
return null;
}

public static ChannelOptions create(AuthAndTLSOptions options) throws IOException {
if (options.authCredentials != null) {
// Credentials from file
try (InputStream authFile = new FileInputStream(options.authCredentials)) {
return create(options, authFile);
return newCallCredentials(authFile, options.authScope);
} catch (FileNotFoundException e) {
String message = String.format("Could not open auth credentials file '%s': %s",
options.authCredentials, e.getMessage());
throw new IOException(message, e);
}
} else {
return create(options, null);
}
// Google Application Default Credentials
return newCallCredentials(null, options.authScope);
}

@VisibleForTesting
static ChannelOptions create(
AuthAndTLSOptions options,
@Nullable InputStream credentialsFile) throws IOException {
final SslContext sslContext =
options.tlsEnabled ? createSSlContext(options.tlsCertificate) : null;

final CallCredentials callCredentials =
options.authEnabled ? createCallCredentials(credentialsFile, options.authScope) : null;

return new ChannelOptions(
sslContext != null, sslContext, options.tlsAuthorityOverride, callCredentials);
}

private static CallCredentials createCallCredentials(@Nullable InputStream credentialsFile,
public static CallCredentials newCallCredentials(@Nullable InputStream credentialsFile,
@Nullable String authScope) throws IOException {
try {
GoogleCredentials creds =
Expand All @@ -105,30 +129,9 @@ private static CallCredentials createCallCredentials(@Nullable InputStream crede
}
return MoreCallCredentials.from(creds);
} catch (IOException e) {
String message = "Failed to init auth credentials for remote caching/execution: "
String message = "Failed to init auth credentials: "
+ e.getMessage();
throw new IOException(message, e);
}
}

private static SslContext createSSlContext(@Nullable String rootCert) throws IOException {
if (rootCert == null) {
try {
return GrpcSslContexts.forClient().build();
} catch (Exception e) {
String message = "Failed to init TLS infrastructure for remote caching/execution: "
+ e.getMessage();
throw new IOException(message, e);
}
} else {
try {
return GrpcSslContexts.forClient().trustManager(new File(rootCert)).build();
} catch (Exception e) {
String message = "Failed to init TLS infrastructure for remote caching/execution using "
+ "'%s' as root certificate: %s";
message = String.format(message, rootCert, e.getMessage());
throw new IOException(message, e);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@

import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.authandtls.AuthAndTLSOptions;
import com.google.devtools.build.lib.authandtls.GrpcUtils;
import com.google.devtools.build.lib.buildeventservice.client.BuildEventServiceClient;
import com.google.devtools.build.lib.buildeventservice.client.BuildEventServiceGrpcClient;
import java.io.IOException;
import java.util.Set;

/**
Expand All @@ -33,11 +35,10 @@ protected Class<BuildEventServiceOptions> optionsClass() {

@Override
protected BuildEventServiceClient createBesClient(BuildEventServiceOptions besOptions,
AuthAndTLSOptions authAndTLSOptions) {
AuthAndTLSOptions authAndTLSOptions) throws IOException {
return new BuildEventServiceGrpcClient(
besOptions.besBackend, authAndTLSOptions.tlsEnabled, authAndTLSOptions.tlsCertificate,
authAndTLSOptions.tlsAuthorityOverride, authAndTLSOptions.authCredentials,
authAndTLSOptions.authScope);
GrpcUtils.newChannel(besOptions.besBackend, authAndTLSOptions),
GrpcUtils.newCallCredentials(authAndTLSOptions));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import com.google.devtools.build.lib.util.io.OutErr;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsProvider;
import java.io.IOException;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Logger;
Expand Down Expand Up @@ -184,7 +185,7 @@ BuildEventStreamer tryCreateStreamer(
@Nullable
private BuildEventTransport tryCreateBesTransport(T besOptions, AuthAndTLSOptions authTlsOptions,
String buildRequestId, String invocationId, ModuleEnvironment moduleEnvironment, Clock clock,
PathConverter pathConverter, EventHandler commandLineReporter) {
PathConverter pathConverter, EventHandler commandLineReporter) throws IOException {
if (isNullOrEmpty(besOptions.besBackend)) {
logger.fine("BuildEventServiceTransport is disabled.");
return null;
Expand Down Expand Up @@ -222,7 +223,7 @@ private BuildEventTransport tryCreateBesTransport(T besOptions, AuthAndTLSOption
protected abstract Class<T> optionsClass();

protected abstract BuildEventServiceClient createBesClient(T besOptions,
AuthAndTLSOptions authAndTLSOptions);
AuthAndTLSOptions authAndTLSOptions) throws IOException;

protected abstract Set<String> whitelistedCommands();
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,12 @@

package com.google.devtools.build.lib.buildeventservice.client;

import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.devtools.build.lib.util.Preconditions.checkNotNull;
import static com.google.devtools.build.lib.util.Preconditions.checkState;
import static java.lang.System.getenv;
import static java.nio.file.Files.newInputStream;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.common.base.Function;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.google.devtools.build.v1.PublishBuildEventGrpc;
Expand All @@ -37,49 +32,23 @@
import io.grpc.ManagedChannel;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.auth.MoreCallCredentials;
import io.grpc.netty.GrpcSslContexts;
import io.grpc.netty.NegotiationType;
import io.grpc.netty.NettyChannelBuilder;
import io.grpc.stub.AbstractStub;
import io.grpc.stub.StreamObserver;
import io.netty.handler.ssl.SslContext;
import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.net.ssl.SSLException;
import org.joda.time.Duration;

/** Implementation of BuildEventServiceClient that uploads data using gRPC. */
public class BuildEventServiceGrpcClient implements BuildEventServiceClient {

private static final Logger logger =
Logger.getLogger(BuildEventServiceGrpcClient.class.getName());

/** Max wait time for a single non-streaming RPC to finish */
private static final Duration RPC_TIMEOUT = Duration.standardSeconds(15);
/** See https://developers.google.com/identity/protocols/application-default-credentials * */
private static final String DEFAULT_APP_CREDENTIALS_ENV_VAR = "GOOGLE_APPLICATION_CREDENTIALS";
/** TODO(eduardocolaco): Scope documentation.* */
private static final String CREDENTIALS_SCOPE =
"https://www.googleapis.com/auth/cloud-build-service";

private final PublishBuildEventStub besAsync;
private final PublishBuildEventBlockingStub besBlocking;
private final ManagedChannel channel;
private final AtomicReference<StreamObserver<PublishBuildToolEventStreamRequest>> streamReference;

public BuildEventServiceGrpcClient(String serverSpec, boolean tlsEnabled,
@Nullable String tlsCertificateFile, @Nullable String tlsAuthorityOverride,
@Nullable String credentialsFile, @Nullable String credentialsScope) {
this(getChannel(serverSpec, tlsEnabled, tlsCertificateFile, tlsAuthorityOverride),
getCallCredentials(credentialsFile, credentialsScope));
}

public BuildEventServiceGrpcClient(
ManagedChannel channel,
@Nullable CallCredentials callCredentials) {
Expand Down Expand Up @@ -183,52 +152,4 @@ public String userReadableError(Throwable t) {
return t.getMessage();
}
}

/**
* Returns call credentials read from the specified file (if non-empty) or from
* env(GOOGLE_APPLICATION_CREDENTIALS) otherwise.
*/
@Nullable
private static CallCredentials getCallCredentials(@Nullable String credentialsFile,
@Nullable String credentialsScope) {
String effectiveScope = credentialsScope != null ? credentialsScope : CREDENTIALS_SCOPE;
try {
if (!isNullOrEmpty(credentialsFile)) {
return MoreCallCredentials.from(
GoogleCredentials.fromStream(newInputStream(Paths.get(credentialsFile)))
.createScoped(ImmutableList.of(effectiveScope)));

} else if (!isNullOrEmpty(getenv(DEFAULT_APP_CREDENTIALS_ENV_VAR))) {
return MoreCallCredentials.from(
GoogleCredentials.getApplicationDefault()
.createScoped(ImmutableList.of(effectiveScope)));
}
} catch (IOException e) {
logger.log(Level.WARNING, "Failed to read credentials", e);
}
return null;
}

/**
* Returns a ManagedChannel to the specified server.
*/
private static ManagedChannel getChannel(String serverSpec, boolean tlsEnabled,
@Nullable String tlsCertificateFile, @Nullable String tlsAuthorityOverride) {
//TODO(buchgr): Use ManagedChannelBuilder once bazel uses a newer gRPC version.
NettyChannelBuilder builder = NettyChannelBuilder.forTarget(serverSpec);
builder.negotiationType(tlsEnabled ? NegotiationType.TLS : NegotiationType.PLAINTEXT);
if (tlsCertificateFile != null) {
try {
SslContext sslContext =
GrpcSslContexts.forClient().trustManager(new File(tlsCertificateFile)).build();
builder.sslContext(sslContext);
} catch (SSLException e) {
throw new RuntimeException(e);
}
}
if (tlsAuthorityOverride != null) {
builder.overrideAuthority(tlsAuthorityOverride);
}
return builder.build();
}
}
Loading

0 comments on commit 37bd5c0

Please sign in to comment.