Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[credentialhelper] Implement invoking credential helper as subprocess #15861

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ java_library(
name = "credentialhelper",
srcs = glob(["*.java"]),
deps = [
"//src/main/java/com/google/devtools/build/lib/events",
"//src/main/java/com/google/devtools/build/lib/shell",
"//src/main/java/com/google/devtools/build/lib/vfs",
"//third_party:auto_value",
"//third_party:error_prone_annotations",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,29 @@

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.io.CharStreams;
import com.google.devtools.build.lib.shell.Subprocess;
import com.google.devtools.build.lib.shell.SubprocessBuilder;
import com.google.devtools.build.lib.vfs.Path;
import com.google.errorprone.annotations.Immutable;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Objects;

/** Wraps an external tool used to obtain credentials. */
@Immutable
public final class CredentialHelper {
private static final Gson GSON = new Gson();

// `Path` is immutable, but not annotated.
@SuppressWarnings("Immutable")
private final Path path;
Expand All @@ -35,5 +52,104 @@ Path getPath() {
return path;
}

// TODO(yannic): Implement running the helper subprocess.
/**
* Fetches credentials for the specified {@link URI} by invoking the credential helper as
* subprocess according to the <a
* href="https://github.com/bazelbuild/proposals/blob/main/designs/2022-06-07-bazel-credential-helpers.md">credential
* helper protocol</a>.
*
* @param environment The environment to run the subprocess in.
* @param uri The {@link URI} to fetch credentials for.
* @return The response from the subprocess.
*/
public GetCredentialsResponse getCredentials(CredentialHelperEnvironment environment, URI uri)
throws InterruptedException, IOException {
Preconditions.checkNotNull(environment);
Preconditions.checkNotNull(uri);

Subprocess process = spawnSubprocess(environment, "get");
try (Reader stdout = new InputStreamReader(process.getInputStream());
Reader stderr = new InputStreamReader(process.getErrorStream())) {
try (Writer stdin =
new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8)) {
GSON.toJson(GetCredentialsRequest.newBuilder().setUri(uri).build(), stdin);
}

process.waitFor();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have a timeout here to avoid hanging Bazel indefinitely? (Just some food for thought, this can be done in a followup)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, I will change the timeout error message before submitting internally, because Duration#toString doesn't produce a human-readable format.

if (process.timedout()) {
throw new IOException(
String.format(
Locale.US,
"Failed to get credentials for '%s' from helper '%s': process did not exit within"
+ " %s",
uri,
path,
environment.getHelperExecutionTimeout()));
}
if (process.exitValue() != 0) {
throw new IOException(
String.format(
Locale.US,
"Failed to get credentials for '%s' from helper '%s': process exited with code %d."
+ " stderr: %s",
uri,
path,
process.exitValue(),
CharStreams.toString(stderr)));
}

try {
GetCredentialsResponse response = GSON.fromJson(stdout, GetCredentialsResponse.class);
if (response == null) {
throw new IOException(
String.format(
Locale.US,
"Failed to get credentials for '%s' from helper '%s': process exited without"
+ " output. stderr: %s",
uri,
path,
CharStreams.toString(stderr)));
}
return response;
} catch (JsonSyntaxException e) {
throw new IOException(
String.format(
Locale.US,
"Failed to get credentials for '%s' from helper '%s': error parsing output. stderr:"
+ " %s",
uri,
path,
CharStreams.toString(stderr)),
e);
}
}
}

private Subprocess spawnSubprocess(CredentialHelperEnvironment environment, String... args)
throws IOException {
Preconditions.checkNotNull(environment);
Preconditions.checkNotNull(args);

return new SubprocessBuilder()
.setArgv(ImmutableList.<String>builder().add(path.getPathString()).add(args).build())
.setWorkingDirectory(environment.getWorkspacePath().getPathFile())
.setEnv(environment.getClientEnvironment())
.setTimeoutMillis(environment.getHelperExecutionTimeout().toMillis())
.start();
}

@Override
public boolean equals(Object o) {
if (o instanceof CredentialHelper) {
CredentialHelper that = (CredentialHelper) o;
return Objects.equals(this.getPath(), that.getPath());
}

return false;
}

@Override
public int hashCode() {
return Objects.hash(getPath());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// 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.authandtls.credentialhelper;

import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.events.Reporter;
import com.google.devtools.build.lib.vfs.Path;
import java.time.Duration;

/** Environment for running {@link CredentialHelper}s in. */
@AutoValue
public abstract class CredentialHelperEnvironment {
/** Returns the reporter for reporting events related to {@link CredentialHelper}s. */
public abstract Reporter getEventReporter();

/**
* Returns the (absolute) path to the workspace.
*
* <p>Used as working directory when invoking the subprocess.
*/
public abstract Path getWorkspacePath();

/**
* Returns the environment from the Bazel client.
*
* <p>Passed as environment variables to the subprocess.
*/
public abstract ImmutableMap<String, String> getClientEnvironment();

/** Returns the execution timeout for the helper subprocess. */
public abstract Duration getHelperExecutionTimeout();

/** Returns a new builder for {@link CredentialHelperEnvironment}. */
public static CredentialHelperEnvironment.Builder newBuilder() {
return new AutoValue_CredentialHelperEnvironment.Builder();
}

/** Builder for {@link CredentialHelperEnvironment}. */
@AutoValue.Builder
public abstract static class Builder {
/** Sets the reporter for reporting events related to {@link CredentialHelper}s. */
public abstract Builder setEventReporter(Reporter reporter);

/**
* Sets the (absolute) path to the workspace to use as working directory when invoking the
* subprocess.
*/
public abstract Builder setWorkspacePath(Path path);

/**
* Sets the environment from the Bazel client to pass as environment variables to the
* subprocess.
*/
public abstract Builder setClientEnvironment(ImmutableMap<String, String> environment);

/** Sets the execution timeout for the helper subprocess. */
public abstract Builder setHelperExecutionTimeout(Duration timeout);

/** Returns the newly constructed {@link CredentialHelperEnvironment}. */
public abstract CredentialHelperEnvironment build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public static Builder newBuilder() {
/** Builder for {@link GetCredentialsResponse}. */
@AutoValue.Builder
public abstract static class Builder {
protected abstract ImmutableMap.Builder<String, ImmutableList<String>> headersBuilder();
public abstract ImmutableMap.Builder<String, ImmutableList<String>> headersBuilder();

/** Returns the newly constructed {@link GetCredentialsResponse}. */
public abstract GetCredentialsResponse build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,31 @@ filegroup(
java_test(
name = "credentialhelper",
srcs = glob(["*.java"]),
data = [
":test_credential_helper",
],
test_class = "com.google.devtools.build.lib.AllTests",
runtime_deps = [
"//src/test/java/com/google/devtools/build/lib:test_runner",
],
deps = [
"//src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper",
"//src/main/java/com/google/devtools/build/lib/events",
"//src/main/java/com/google/devtools/build/lib/util:os",
"//src/main/java/com/google/devtools/build/lib/vfs",
"//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
"//src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs",
"//third_party:gson",
"//third_party:guava",
"//third_party:junit4",
"//third_party:truth",
"@bazel_tools//tools/java/runfiles",
],
)

py_binary(
name = "test_credential_helper",
srcs = [
"test_credential_helper.py",
],
)
Loading