blob: 420dcce09afad00013683e1696f8eb0f3af46ca4 [file] [log] [blame]
// Copyright 2022 The Bazel 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.
package com.google.devtools.build.lib.remote;
import static com.google.common.truth.Truth.assertThat;
import static com.google.devtools.build.lib.remote.util.Utils.getFromFuture;
import build.bazel.remote.execution.v2.Digest;
import build.bazel.remote.execution.v2.RequestMetadata;
import com.google.api.client.json.GenericJson;
import com.google.api.client.json.gson.GsonFactory;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.devtools.build.lib.authandtls.AuthAndTLSOptions;
import com.google.devtools.build.lib.authandtls.CallCredentialsProvider;
import com.google.devtools.build.lib.authandtls.GoogleAuthUtils;
import com.google.devtools.build.lib.clock.JavaClock;
import com.google.devtools.build.lib.remote.RemoteRetrier.ExponentialBackoff;
import com.google.devtools.build.lib.remote.Retrier.Backoff;
import com.google.devtools.build.lib.remote.common.RemoteActionExecutionContext;
import com.google.devtools.build.lib.remote.common.RemotePathResolver;
import com.google.devtools.build.lib.remote.grpc.ChannelConnectionFactory;
import com.google.devtools.build.lib.remote.options.RemoteOptions;
import com.google.devtools.build.lib.remote.util.DigestUtil;
import com.google.devtools.build.lib.remote.util.TestUtils;
import com.google.devtools.build.lib.remote.util.TracingMetadataUtils;
import com.google.devtools.build.lib.testutil.Scratch;
import com.google.devtools.build.lib.util.io.FileOutErr;
import com.google.devtools.build.lib.vfs.DigestHashFunction;
import com.google.devtools.build.lib.vfs.FileSystem;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.SyscallCache;
import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
import com.google.devtools.common.options.Options;
import io.grpc.CallCredentials;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ManagedChannel;
import io.grpc.MethodDescriptor;
import io.grpc.Server;
import io.grpc.inprocess.InProcessChannelBuilder;
import io.grpc.inprocess.InProcessServerBuilder;
import io.grpc.util.MutableHandlerRegistry;
import io.reactivex.rxjava3.core.Single;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import org.junit.After;
import org.junit.Before;
class GrpcCacheClientTestBase {
protected static final DigestUtil DIGEST_UTIL =
new DigestUtil(SyscallCache.NO_CACHE, DigestHashFunction.SHA256);
protected FileSystem fs;
protected Path execRoot;
protected FileOutErr outErr;
protected FakeActionInputFileCache fakeFileCache;
protected final MutableHandlerRegistry serviceRegistry = new MutableHandlerRegistry();
protected final String fakeServerName = "fake server for " + getClass();
protected Server fakeServer;
protected RemoteActionExecutionContext context;
protected RemotePathResolver remotePathResolver;
protected ListeningScheduledExecutorService retryService;
private final ArrayList<ReferenceCountedChannel> channels = new ArrayList<>();
@Before
public final void setUp() throws Exception {
// Use a mutable service registry for later registering the service impl for each test case.
fakeServer =
InProcessServerBuilder.forName(fakeServerName)
.fallbackHandlerRegistry(serviceRegistry)
.directExecutor()
.build()
.start();
Chunker.setDefaultChunkSizeForTesting(1000); // Enough for everything to be one chunk.
fs = new InMemoryFileSystem(new JavaClock(), DigestHashFunction.SHA256);
execRoot = fs.getPath("/execroot/main");
execRoot.createDirectoryAndParents();
fakeFileCache = new FakeActionInputFileCache(execRoot);
remotePathResolver = RemotePathResolver.createDefault(execRoot);
Path stdout = fs.getPath("/tmp/stdout");
Path stderr = fs.getPath("/tmp/stderr");
stdout.getParentDirectory().createDirectoryAndParents();
stderr.getParentDirectory().createDirectoryAndParents();
outErr = new FileOutErr(stdout, stderr);
RequestMetadata metadata =
TracingMetadataUtils.buildMetadata(
"none", "none", Digest.getDefaultInstance().getHash(), null);
context = RemoteActionExecutionContext.create(metadata);
retryService = MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(1));
}
@After
public void tearDown() throws Exception {
channels.forEach(ReferenceCountedChannel::release);
retryService.shutdownNow();
retryService.awaitTermination(
com.google.devtools.build.lib.testutil.TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
fakeServer.shutdownNow();
fakeServer.awaitTermination();
}
protected GrpcCacheClient newClient(RemoteOptions remoteOptions) throws IOException {
return newClient(remoteOptions, () -> new ExponentialBackoff(remoteOptions));
}
protected GrpcCacheClient newClient(
RemoteOptions remoteOptions, Supplier<Backoff> backoffSupplier) throws IOException {
AuthAndTLSOptions authTlsOptions = Options.getDefaults(AuthAndTLSOptions.class);
authTlsOptions.useGoogleDefaultCredentials = true;
authTlsOptions.googleCredentials = "/execroot/main/creds.json";
authTlsOptions.googleAuthScopes = ImmutableList.of("dummy.scope");
GenericJson json = new GenericJson();
json.put("type", "authorized_user");
json.put("client_id", "some_client");
json.put("client_secret", "foo");
json.put("refresh_token", "bar");
Scratch scratch = new Scratch();
scratch.file(authTlsOptions.googleCredentials, new GsonFactory().toString(json));
CallCredentialsProvider callCredentialsProvider;
try (InputStream in = scratch.resolve(authTlsOptions.googleCredentials).getInputStream()) {
callCredentialsProvider =
GoogleAuthUtils.newCallCredentialsProvider(
GoogleAuthUtils.newGoogleCredentialsFromFile(in, authTlsOptions.googleAuthScopes));
}
CallCredentials creds = callCredentialsProvider.getCallCredentials();
RemoteRetrier retrier =
TestUtils.newRemoteRetrier(
backoffSupplier, RemoteRetrier.RETRIABLE_GRPC_ERRORS, retryService);
ReferenceCountedChannel channel =
new ReferenceCountedChannel(
new ChannelConnectionFactory() {
@Override
public Single<? extends ChannelConnection> create() {
ManagedChannel ch =
InProcessChannelBuilder.forName(fakeServerName)
.directExecutor()
.intercept(new CallCredentialsInterceptor(creds))
.intercept(TracingMetadataUtils.newCacheHeadersInterceptor(remoteOptions))
.build();
return Single.just(new ChannelConnection(ch));
}
@Override
public int maxConcurrency() {
return 100;
}
});
channels.add(channel);
return new GrpcCacheClient(
channel, callCredentialsProvider, remoteOptions, retrier, DIGEST_UTIL);
}
protected static byte[] downloadBlob(
RemoteActionExecutionContext context, GrpcCacheClient cacheClient, Digest digest)
throws IOException, InterruptedException {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
getFromFuture(cacheClient.downloadBlob(context, digest, out));
return out.toByteArray();
}
}
private static class CallCredentialsInterceptor implements ClientInterceptor {
private final CallCredentials credentials;
public CallCredentialsInterceptor(CallCredentials credentials) {
this.credentials = credentials;
}
@Override
public <RequestT, ResponseT> ClientCall<RequestT, ResponseT> interceptCall(
MethodDescriptor<RequestT, ResponseT> method, CallOptions callOptions, Channel next) {
assertThat(callOptions.getCredentials()).isEqualTo(credentials);
// Remove the call credentials to allow testing with dummy ones.
return next.newCall(method, callOptions.withCallCredentials(null));
}
}
}