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

Try Google Application Default Credentials for GCR (gcr.io) auth #1902

Merged
merged 18 commits into from
Aug 14, 2019
Merged
Show file tree
Hide file tree
Changes from 11 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
8 changes: 7 additions & 1 deletion jib-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,16 @@ configurations {

dependencies {
// Make sure these are consistent with jib-maven-plugin.

// For Google libraries, check <http-client-bom.version>, <google.auth.version>, <guava.version>,
// ... in https://github.com/googleapis/google-cloud-java/blob/master/google-cloud-clients/pom.xml
// for best compatibility.
implementation 'com.google.http-client:google-http-client:1.31.0'
implementation 'com.google.http-client:google-http-client-apache-v2:1.31.0'
implementation 'com.google.auth:google-auth-library-oauth2-http:0.16.2'
implementation 'com.google.guava:guava:28.0-jre'

implementation 'org.apache.commons:commons-compress:1.18'
implementation 'com.google.guava:guava:27.0.1-jre'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.9.2'
implementation 'org.ow2.asm:asm:7.0'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public Optional<Credential> call() throws CredentialRetrievalException {
}

// If no credentials found, give an info (not warning because in most cases, the base image is
// public and does not need extra credentials) and return null.
// public and does not need extra credentials) and return empty.
eventHandlers.dispatch(
LogEvent.info("No credentials could be retrieved for registry " + registry));
return Optional.empty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package com.google.cloud.tools.jib.frontend;

import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.tools.jib.api.Credential;
import com.google.cloud.tools.jib.api.CredentialRetriever;
import com.google.cloud.tools.jib.api.ImageReference;
Expand All @@ -30,10 +32,11 @@
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/** Static factories for various {@link CredentialRetriever}s. */
public class CredentialRetrieverFactory {
Expand All @@ -45,12 +48,21 @@ interface DockerCredentialHelperFactory {
DockerCredentialHelper create(String registry, Path credentialHelper);
}

/**
* Defines common credential helpers to use as defaults. Maps from registry suffix to credential
* helper suffix.
*/
private static final ImmutableMap<String, String> COMMON_CREDENTIAL_HELPERS =
ImmutableMap.of("gcr.io", "gcr", "amazonaws.com", "ecr-login");
/** Used for passing in mock {@link GoogleCredentials} for testing. */
@VisibleForTesting
@FunctionalInterface
interface GoogleCredentialsProvider {
GoogleCredentials get() throws IOException;
}

// com.google.api.services.storage.StorageScopes.DEVSTORAGE_READ_WRITE
chanseokoh marked this conversation as resolved.
Show resolved Hide resolved
private static final String OAUTH_SCOPE_STORAGE_READ_WRITE =
"https://www.googleapis.com/auth/devstorage.read_write";

/** Mapping between well-known credential helpers and registries (suffixes). */
private static final ImmutableMap<String, String> WELL_KNOWN_CREDENTIAL_HELPERS =
ImmutableMap.of(
"gcr.io", "docker-credential-gcr", "amazonaws.com", "docker-credential-ecr-login");

/**
* Creates a new {@link CredentialRetrieverFactory} for an image.
Expand All @@ -61,32 +73,28 @@ interface DockerCredentialHelperFactory {
*/
public static CredentialRetrieverFactory forImage(
ImageReference imageReference, Consumer<LogEvent> logger) {
return new CredentialRetrieverFactory(imageReference, logger, DockerCredentialHelper::new);
}

/**
* Creates a new {@link CredentialRetrieverFactory} for an image.
*
* @param imageReference the image the credential are for
* @return a new {@link CredentialRetrieverFactory}
*/
public static CredentialRetrieverFactory forImage(ImageReference imageReference) {
return new CredentialRetrieverFactory(
imageReference, logEvent -> {}, DockerCredentialHelper::new);
imageReference,
logger,
DockerCredentialHelper::new,
GoogleCredentials::getApplicationDefault);
}

private final ImageReference imageReference;
private final Consumer<LogEvent> logger;
private final DockerCredentialHelperFactory dockerCredentialHelperFactory;
private final GoogleCredentialsProvider googleCredentialsProvider;
chanseokoh marked this conversation as resolved.
Show resolved Hide resolved

@VisibleForTesting
CredentialRetrieverFactory(
ImageReference imageReference,
Consumer<LogEvent> logger,
DockerCredentialHelperFactory dockerCredentialHelperFactory) {
DockerCredentialHelperFactory dockerCredentialHelperFactory,
GoogleCredentialsProvider googleCredentialsProvider) {
this.imageReference = imageReference;
this.logger = logger;
this.dockerCredentialHelperFactory = dockerCredentialHelperFactory;
this.googleCredentialsProvider = googleCredentialsProvider;
}

/**
Expand Down Expand Up @@ -125,8 +133,6 @@ public CredentialRetriever dockerCredentialHelper(String credentialHelper) {
*/
public CredentialRetriever dockerCredentialHelper(Path credentialHelper) {
return () -> {
logger.accept(LogEvent.info("Checking credentials from " + credentialHelper));

try {
return Optional.of(retrieveFromDockerCredentialHelper(credentialHelper));

Expand All @@ -143,33 +149,25 @@ public CredentialRetriever dockerCredentialHelper(Path credentialHelper) {
}

/**
* Creates a new {@link CredentialRetriever} that tries common Docker credential helpers to
* Creates a new {@link CredentialRetriever} that tries well-known Docker credential helpers to
* retrieve credentials based on the registry of the image, such as {@code docker-credential-gcr}
* for images with the registry as {@code gcr.io}.
* for images with the registry ending with {@code gcr.io}.
*
* @return a new {@link CredentialRetriever}
*/
public CredentialRetriever inferCredentialHelper() {
List<String> inferredCredentialHelperSuffixes = new ArrayList<>();
for (String registrySuffix : COMMON_CREDENTIAL_HELPERS.keySet()) {
if (!imageReference.getRegistry().endsWith(registrySuffix)) {
continue;
}
String inferredCredentialHelperSuffix = COMMON_CREDENTIAL_HELPERS.get(registrySuffix);
if (inferredCredentialHelperSuffix == null) {
throw new IllegalStateException("No COMMON_CREDENTIAL_HELPERS should be null");
}
inferredCredentialHelperSuffixes.add(inferredCredentialHelperSuffix);
}
public CredentialRetriever wellKnownCredentialHelpers() {
briandealwis marked this conversation as resolved.
Show resolved Hide resolved
List<String> wellKnownCredentialHelpers =
WELL_KNOWN_CREDENTIAL_HELPERS
.keySet()
.stream()
.filter(imageReference.getRegistry()::endsWith)
.map(key -> WELL_KNOWN_CREDENTIAL_HELPERS.get(key))
.collect(Collectors.toList());
chanseokoh marked this conversation as resolved.
Show resolved Hide resolved

return () -> {
for (String inferredCredentialHelperSuffix : inferredCredentialHelperSuffixes) {
for (String credentialHelper : wellKnownCredentialHelpers) {
try {
return Optional.of(
retrieveFromDockerCredentialHelper(
Paths.get(
DockerCredentialHelper.CREDENTIAL_HELPER_PREFIX
+ inferredCredentialHelperSuffix)));
return Optional.of(retrieveFromDockerCredentialHelper(Paths.get(credentialHelper)));

} catch (CredentialHelperNotFoundException
| CredentialHelperUnhandledServerUrlException ex) {
Expand Down Expand Up @@ -213,18 +211,55 @@ public CredentialRetriever dockerConfig(Path dockerConfigFile) {
new DockerConfigCredentialRetriever(imageReference.getRegistry(), dockerConfigFile));
}

/**
* Creates a new {@link CredentialRetriever} that tries to retrieve credentials from <a
* href="https://cloud.google.com/docs/authentication/production">Google Application Default
* Credentials.</a>
*
* @return a new {@link CredentialRetriever}
* @see <a
* href="https://cloud.google.com/docs/authentication/production">https://cloud.google.com/docs/authentication/production</a>
*/
public CredentialRetriever googleApplicationDefaultCredentials() {
return () -> {
try {
if (imageReference.getRegistry().endsWith("gcr.io")) {
GoogleCredentials googleCredentials = googleCredentialsProvider.get();
logger.accept(LogEvent.info("Google ADC found"));
if (googleCredentials.createScopedRequired()) { // Not scoped if service account.
// The short-lived OAuth2 access token generated from the service account will have
// one-hour expiry (as of Aug 2019). Instead of using an access token, It is technically
// possible to use the service account private key to auth with GCR, but it does not
// worth writing complex code to achieve that.
logger.accept(LogEvent.info("ADC is a service account. Set GCS read-write scope"));
chanseokoh marked this conversation as resolved.
Show resolved Hide resolved
List<String> scope = Collections.singletonList(OAUTH_SCOPE_STORAGE_READ_WRITE);
googleCredentials = googleCredentials.createScoped(scope);
}
googleCredentials.refreshIfExpired();

logGotCredentialsFrom("Google Application Default Credentials");
AccessToken accessToken = googleCredentials.getAccessToken();
// https://cloud.google.com/container-registry/docs/advanced-authentication#access_token
return Optional.of(Credential.from("oauth2accesstoken", accessToken.getTokenValue()));
}

} catch (IOException ex) { // Includes the case where ADC is simply not available.
logger.accept(
LogEvent.info("ADC not present or error fetching access token: " + ex.getMessage()));
}
return Optional.empty();
};
}

@VisibleForTesting
CredentialRetriever dockerConfig(
DockerConfigCredentialRetriever dockerConfigCredentialRetriever) {
return () -> {
try {
Optional<Credential> dockerConfigCredentials =
dockerConfigCredentialRetriever.retrieve(logger);
if (dockerConfigCredentials.isPresent()) {
logger.accept(
LogEvent.info(
"Using credentials from Docker config for " + imageReference.getRegistry()));
return dockerConfigCredentials;
Optional<Credential> credentials = dockerConfigCredentialRetriever.retrieve(logger);
if (credentials.isPresent()) {
logGotCredentialsFrom("credentials from Docker config");
return credentials;
}

} catch (IOException ex) {
Expand All @@ -241,7 +276,7 @@ private Credential retrieveFromDockerCredentialHelper(Path credentialHelper)
dockerCredentialHelperFactory
.create(imageReference.getRegistry(), credentialHelper)
.retrieve();
logGotCredentialsFrom(credentialHelper.getFileName().toString());
logGotCredentialsFrom("credentials from " + credentialHelper.getFileName().toString());
return credentials;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ public DockerConfigCredentialRetriever(String registry) {
this(registry, DOCKER_CONFIG_FILE);
}

@VisibleForTesting
public DockerConfigCredentialRetriever(String registry, Path dockerConfigFile) {
this.registry = registry;
this.dockerConfigFile = dockerConfigFile;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@
*/
public class DockerCredentialHelper {

public static final String CREDENTIAL_HELPER_PREFIX = "docker-credential-";

private final String serverUrl;
private final Path credentialHelper;

Expand All @@ -65,7 +63,7 @@ public DockerCredentialHelper(String serverUrl, Path credentialHelper) {
}

DockerCredentialHelper(String registry, String credentialHelperSuffix) {
this(registry, Paths.get(CREDENTIAL_HELPER_PREFIX + credentialHelperSuffix));
this(registry, Paths.get("docker-credential-" + credentialHelperSuffix));
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if we should get rid of this method and push knowledge of the docker-credential- prefix to the callers/creators?

Copy link
Member Author

@chanseokoh chanseokoh Aug 13, 2019

Choose a reason for hiding this comment

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

Let me take care of this in a separate PR. (#1920)

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import com.google.cloud.tools.jib.image.json.OCIManifestTemplate;
import com.google.cloud.tools.jib.image.json.V22ManifestTemplate;
import com.google.cloud.tools.jib.registry.credentials.CredentialRetrievalException;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.MoreExecutors;
Expand Down Expand Up @@ -225,7 +224,7 @@ public void testContainerize_executorCreated() throws Exception {

Containerizer mockContainerizer = createMockContainerizer();

jibContainerBuilder.containerize(mockContainerizer, Suppliers.ofInstance(mockExecutorService));
jibContainerBuilder.containerize(mockContainerizer, () -> mockExecutorService);

Mockito.verify(mockExecutorService).shutdown();
}
Expand Down
Loading