Skip to content

Commit

Permalink
Make compatible with Gradle Config Cache (#92)
Browse files Browse the repository at this point in the history
* bring build.gradle up to date

* update gradle to 8.1.1

* introduce CommandExecutor in order to provide different implementations for maven and gradle:
- keep using ProcessBuilder for Maven
- Use ProviderFactory for Gradle

* update gradle to 8.7

* catch and handle exec exception

---------

Co-authored-by: Hanzhen Yi <hzyi@google.com>
  • Loading branch information
bjoernmayer and yihanzhen authored Dec 3, 2024
1 parent c9b7f29 commit fa581f3
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 45 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.google.cloud.artifactregistry.auth;

import java.io.IOException;

public interface CommandExecutor {
public CommandExecutorResult executeCommand(
String command,
String... args
) throws IOException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.google.cloud.artifactregistry.auth;

public class CommandExecutorResult {
public final int exitCode;
public final String stdOut;
public final String stdErr;

public CommandExecutorResult(
int exitCode,
String stdOut,
String stdErr
) {
this.exitCode = exitCode;
this.stdOut = stdOut;
this.stdErr = stdErr;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@
@FunctionalInterface
public interface CredentialProvider {

Credentials getCredential() throws IOException;
Credentials getCredential(CommandExecutor commandExecutor) throws IOException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ public static DefaultCredentialProvider getInstance() {
// Private constructor so that they must use the singleton.
private DefaultCredentialProvider(){}

public Credentials getCredential() throws IOException {
public Credentials getCredential(CommandExecutor commandExecutor) throws IOException {
synchronized (this) {
if (cachedCredentials == null) {
LOGGER.info("Initializing Credentials...");
cachedCredentials = makeGoogleCredentials();
cachedCredentials = makeGoogleCredentials(commandExecutor);
}
refreshIfNeeded();
return cachedCredentials;
Expand All @@ -66,7 +66,7 @@ public void refreshIfNeeded() throws IOException {
}
}

public GoogleCredentials makeGoogleCredentials() throws IOException {
public GoogleCredentials makeGoogleCredentials(CommandExecutor commandExecutor) throws IOException {
LOGGER.debug("ArtifactRegistry: Retrieving credentials...");
GoogleCredentials credentials;

Expand All @@ -82,7 +82,7 @@ public GoogleCredentials makeGoogleCredentials() throws IOException {

LOGGER.debug("Trying gcloud credentials...");
try {
credentials = GcloudCredentials.tryCreateGcloudCredentials();
credentials = GcloudCredentials.tryCreateGcloudCredentials(commandExecutor);
LOGGER.info("Using credentials retrieved from gcloud.");
return credentials;
} catch (IOException ex) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,24 @@ public class GcloudCredentials extends GoogleCredentials {
private static final String KEY_ACCESS_TOKEN = "access_token";
private static final String KEY_TOKEN_EXPIRY = "token_expiry";

private final CommandExecutor commandExecutor;

public GcloudCredentials(AccessToken initialToken) {

public GcloudCredentials(
AccessToken initialToken,
CommandExecutor commandExecutor
) {
super(initialToken);
this.commandExecutor = commandExecutor;
}



/**
* Tries to get credentials from gcloud. Returns null if credentials are not available.
* @return The Credentials from gcloud
* @throws IOException if there was an error retrieving credentials from gcloud
*/
public static GcloudCredentials tryCreateGcloudCredentials() throws IOException {
return new GcloudCredentials(validateAccessToken(getGcloudAccessToken()));
public static GcloudCredentials tryCreateGcloudCredentials(CommandExecutor commandExecutor) throws IOException {
return new GcloudCredentials(validateAccessToken(getGcloudAccessToken(commandExecutor)), commandExecutor);
}

private static String gCloudCommand() {
Expand All @@ -66,7 +70,7 @@ private static String gCloudCommand() {
@Override
public AccessToken refreshAccessToken() throws IOException {
LOGGER.info("Refreshing gcloud credentials...");
return validateAccessToken(getGcloudAccessToken());
return validateAccessToken(getGcloudAccessToken(this.commandExecutor));
}

// Checks that the token is valid, throws IOException if it is expired.
Expand All @@ -82,16 +86,15 @@ private static AccessToken validateAccessToken(AccessToken token) throws IOExcep
return token;
}

private static AccessToken getGcloudAccessToken() throws IOException {
ProcessBuilder processBuilder = new ProcessBuilder();
String gcloud = gCloudCommand();
processBuilder.command(gcloud, "config", "config-helper", "--format=json(credential)");
Process process = processBuilder.start();
private static AccessToken getGcloudAccessToken(CommandExecutor commandExecutor) throws IOException {
try {
int exitCode = process.waitFor();
String stdOut = readStreamToString(process.getInputStream());
String gcloud = gCloudCommand();
CommandExecutorResult commandExecutorResult = commandExecutor.executeCommand(gcloud, "config", "config-helper", "--format=json(credential)");
int exitCode = commandExecutorResult.exitCode;
String stdOut = commandExecutorResult.stdOut;

if (exitCode != 0) {
String stdErr = readStreamToString(process.getErrorStream());
String stdErr = commandExecutorResult.stdErr;
throw new IOException(String.format("gcloud exited with status: %d\nOutput:\n%s\nError Output:\n%s\n",
exitCode, stdOut, stdErr));
}
Expand All @@ -108,24 +111,8 @@ private static AccessToken getGcloudAccessToken() throws IOException {
df.setTimeZone(TimeZone.getTimeZone("UTC"));
Date expiry = df.parse((String) credential.get(KEY_TOKEN_EXPIRY));
return new AccessToken((String) credential.get(KEY_ACCESS_TOKEN), expiry);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException(e);
} catch (ParseException e) {
throw new IOException("Failed to parse timestamp from gcloud output", e);
}
}

// Reads a stream to a string, this code is basically copied from 'copyReaderToBuilder' from
// com.google.io.CharStreams in the Guava library.
private static String readStreamToString(InputStream input) throws IOException {
InputStreamReader reader = new InputStreamReader(input);
StringBuilder output = new StringBuilder();
char[] buf = new char[0x800];
int nRead;
while ((nRead = reader.read(buf)) != -1) {
output.append(buf, 0, nRead);
}
return output.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.artifactregistry.auth.CommandExecutor;
import com.google.cloud.artifactregistry.auth.CredentialProvider;
import com.google.cloud.artifactregistry.auth.DefaultCredentialProvider;
import java.io.IOException;
Expand All @@ -37,6 +38,7 @@
import org.gradle.api.internal.artifacts.repositories.DefaultMavenArtifactRepository;
import org.gradle.api.invocation.Gradle;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.ProviderFactory;
import org.gradle.api.publish.PublishingExtension;
import org.gradle.api.tasks.Input;
import org.gradle.internal.authentication.DefaultBasicAuthentication;
Expand All @@ -51,10 +53,12 @@ public class ArtifactRegistryGradlePlugin implements Plugin<Object> {
static class ArtifactRegistryPasswordCredentials implements PasswordCredentials {
private String username;
private String password;
private final CommandExecutor commandExecutor;

ArtifactRegistryPasswordCredentials(String username, String password) {
ArtifactRegistryPasswordCredentials(String username, String password, CommandExecutor commandExecutor) {
this.username = username;
this.password = password;
this.commandExecutor = commandExecutor;
}

@Input
Expand Down Expand Up @@ -86,11 +90,24 @@ public void setPassword(String password) {
public void apply(Object o) {
ArtifactRegistryPasswordCredentials crd = null;
try {
GoogleCredentials credentials = (GoogleCredentials)credentialProvider.getCredential();
ProviderFactory providerFactory;
if (o instanceof Project) {
providerFactory = ((Project) o).getProviders();
} else if (o instanceof Gradle) {
providerFactory = ((Gradle) o).getRootProject().getProviders();
} else if (o instanceof Settings) {
providerFactory = ((Settings) o).getProviders();
} else {
logger.info("Failed to get access token from gcloud or Application Default Credentials due to unknown script type " + o);
return;
}
CommandExecutor commandExecutor = new ProviderFactoryCommandExecutor(providerFactory);

GoogleCredentials credentials = (GoogleCredentials)credentialProvider.getCredential(commandExecutor);
credentials.refreshIfExpired();
AccessToken accessToken = credentials.getAccessToken();
String token = accessToken.getTokenValue();
crd = new ArtifactRegistryPasswordCredentials("oauth2accesstoken", token);
crd = new ArtifactRegistryPasswordCredentials("oauth2accesstoken", token, commandExecutor);
} catch (IOException e) {
logger.info("Failed to get access token from gcloud or Application Default Credentials", e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.google.cloud.artifactregistry.gradle.plugin;

import com.google.cloud.artifactregistry.auth.CommandExecutor;
import com.google.cloud.artifactregistry.auth.CommandExecutorResult;
import org.gradle.api.provider.ProviderFactory;
import org.gradle.process.ExecOutput;
import org.gradle.process.internal.ExecException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class ProviderFactoryCommandExecutor implements CommandExecutor {
private final ProviderFactory providerFactory;

public ProviderFactoryCommandExecutor(ProviderFactory providerFactory) {
this.providerFactory = providerFactory;
}

@Override
public CommandExecutorResult executeCommand(String command, String... args) throws IOException {
List<String> argList = new ArrayList<>();
argList.add(command);
argList.addAll(Arrays.asList(args));

ExecOutput execOutput;
try {
execOutput = providerFactory.exec(execSpec -> {
execSpec.commandLine(argList);
});
} catch (ExecException e) {
// Downstream cannot handle Gradle specific exceptions
throw new IOException(e);
}

int exitCode = execOutput.getResult().get().getExitValue();
String stdOut = execOutput.getStandardOutput().getAsText().get();
String stdErr = execOutput.getStandardError().getAsText().get();

return new CommandExecutorResult(
exitCode,
stdOut,
stdErr
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ private InputStream getInputStream(Resource resource)
protected void openConnectionInternal() throws ConnectionException, AuthenticationException {
HttpTransport httpTransport = httpTransportFactory.create();
try {
credentials = credentialProvider.getCredential();
credentials = credentialProvider.getCredential(new ProcessBuilderCommandExecutor());
HttpRequestInitializer requestInitializer = new ArtifactRegistryRequestInitializer(credentials, this.getReadTimeout());
requestFactory = httpTransport.createRequestFactory(requestInitializer);
hasCredentials = true;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.google.cloud.artifactregistry.wagon;

import com.google.api.client.util.GenericData;
import com.google.auth.oauth2.AccessToken;
import com.google.cloud.artifactregistry.auth.CommandExecutor;
import com.google.cloud.artifactregistry.auth.CommandExecutorResult;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class ProcessBuilderCommandExecutor implements CommandExecutor {
@Override
public CommandExecutorResult executeCommand(String command, String... args) throws IOException {
List<String> argList = new ArrayList<>();
argList.add(command);
argList.addAll(Arrays.asList(args));

ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command(argList);
Process process = processBuilder.start();

try {
int exitCode = process.waitFor();
String stdOut = readStreamToString(process.getInputStream());
String stdErr = readStreamToString(process.getErrorStream());

return new CommandExecutorResult(exitCode, stdOut, stdErr);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException(e);
}
}

// Reads a stream to a string, this code is basically copied from 'copyReaderToBuilder' from
// com.google.io.CharStreams in the Guava library.
private static String readStreamToString(InputStream input) throws IOException {
InputStreamReader reader = new InputStreamReader(input);
StringBuilder output = new StringBuilder();
char[] buf = new char[0x800];
int nRead;
while ((nRead = reader.read(buf)) != -1) {
output.append(buf, 0, nRead);
}
return output.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.google.auth.Credentials;
import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.artifactregistry.auth.CommandExecutor;
import com.google.cloud.artifactregistry.auth.CredentialProvider;
import java.io.FileWriter;
import java.io.IOException;
Expand Down Expand Up @@ -101,7 +102,7 @@ public void testAuthenticatedGet() throws Exception {
.setLowLevelHttpResponse(new MockLowLevelHttpResponse().setContent("test content"))
.build();
ArtifactRegistryWagon wagon = new ArtifactRegistryWagon();
wagon.setCredentialProvider(() -> GoogleCredentials.create(new AccessToken("test-access-token", Date.from(
wagon.setCredentialProvider((commandExecutor) -> GoogleCredentials.create(new AccessToken("test-access-token", Date.from(
Instant.now().plusSeconds(1000)))));
wagon.setHttpTransportFactory(() -> transport);
wagon.connect(new Repository("my-repo", REPO_URL));
Expand All @@ -119,7 +120,7 @@ public void testAuthenticatedPut() throws Exception {
MockHttpTransport transport = new MockHttpTransport.Builder()
.setLowLevelHttpResponse(new MockLowLevelHttpResponse()).build();
ArtifactRegistryWagon wagon = new ArtifactRegistryWagon();
wagon.setCredentialProvider(() -> GoogleCredentials.create(new AccessToken("test-access-token", Date.from(
wagon.setCredentialProvider((commandExecutor) -> GoogleCredentials.create(new AccessToken("test-access-token", Date.from(
Instant.now().plusSeconds(1000)))));
wagon.setHttpTransportFactory(() -> transport);
wagon.connect(new Repository("my-repo", REPO_URL));
Expand Down Expand Up @@ -174,7 +175,7 @@ public void testHeadExists() throws Exception {
.setLowLevelHttpResponse(new MockLowLevelHttpResponse().setStatusCode(HttpStatusCodes.STATUS_CODE_OK))
.build();
ArtifactRegistryWagon wagon = new ArtifactRegistryWagon();
wagon.setCredentialProvider(() -> GoogleCredentials.create(new AccessToken("test-access-token", Date.from(
wagon.setCredentialProvider((commandExecutor) -> GoogleCredentials.create(new AccessToken("test-access-token", Date.from(
Instant.now().plusSeconds(1000)))));
wagon.setHttpTransportFactory(() -> transport);
wagon.connect(new Repository("my-repo", REPO_URL));
Expand All @@ -185,7 +186,7 @@ public void testHeadExists() throws Exception {
public void testHeadNotFound() throws Exception {
MockHttpTransport transport = failingTransportWithStatus(HttpStatusCodes.STATUS_CODE_NOT_FOUND);
ArtifactRegistryWagon wagon = new ArtifactRegistryWagon();
wagon.setCredentialProvider(() -> GoogleCredentials.create(new AccessToken("test-access-token", Date.from(
wagon.setCredentialProvider((commandExecutor) -> GoogleCredentials.create(new AccessToken("test-access-token", Date.from(
Instant.now().plusSeconds(1000)))));
wagon.setHttpTransportFactory(() -> transport);
wagon.connect(new Repository("my-repo", REPO_URL));
Expand Down Expand Up @@ -227,7 +228,7 @@ public FailingCredentialProvider(IOException e) {
}

@Override
public Credentials getCredential() throws IOException {
public Credentials getCredential(CommandExecutor commandExecutor) throws IOException {
throw exception;
}
}
Expand All @@ -243,4 +244,4 @@ private String readStringFromFile(File f) throws IOException {
return new String(encoded, Charset.defaultCharset());
}

}
}

0 comments on commit fa581f3

Please sign in to comment.