diff --git a/jib-core/build.gradle b/jib-core/build.gradle index d6a32062fb..bf1bf7585a 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -37,10 +37,16 @@ configurations { dependencies { // Make sure these are consistent with jib-maven-plugin. + + // For Google libraries, check , , , + // ... 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' diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java index fa7422a557..b149b213fe 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RetrieveRegistryCredentialsStep.java @@ -87,7 +87,7 @@ public Optional 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(); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java index 14ba14124b..382df76d1e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java @@ -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; @@ -30,8 +32,9 @@ 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.Map; import java.util.Optional; import java.util.function.Consumer; @@ -45,12 +48,25 @@ 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 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 + // OAuth2 credentials require at least the GCS write scope for GCR push. We need to manually set + // this scope for "OAuth2 credentials" instantiated from a service account, which are not scoped + // (i.e., createScopedRequired() returns true). Note that for a service account, the IAM roles of + // the service account determine the IAM permissions. + 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 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. @@ -61,32 +77,28 @@ interface DockerCredentialHelperFactory { */ public static CredentialRetrieverFactory forImage( ImageReference imageReference, Consumer 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 logger; private final DockerCredentialHelperFactory dockerCredentialHelperFactory; + private final GoogleCredentialsProvider googleCredentialsProvider; @VisibleForTesting CredentialRetrieverFactory( ImageReference imageReference, Consumer logger, - DockerCredentialHelperFactory dockerCredentialHelperFactory) { + DockerCredentialHelperFactory dockerCredentialHelperFactory, + GoogleCredentialsProvider googleCredentialsProvider) { this.imageReference = imageReference; this.logger = logger; this.dockerCredentialHelperFactory = dockerCredentialHelperFactory; + this.googleCredentialsProvider = googleCredentialsProvider; } /** @@ -125,8 +137,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)); @@ -143,33 +153,21 @@ 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 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() { return () -> { - for (String inferredCredentialHelperSuffix : inferredCredentialHelperSuffixes) { + for (Map.Entry entry : WELL_KNOWN_CREDENTIAL_HELPERS.entrySet()) { try { - return Optional.of( - retrieveFromDockerCredentialHelper( - Paths.get( - DockerCredentialHelper.CREDENTIAL_HELPER_PREFIX - + inferredCredentialHelperSuffix))); + String registrySuffix = entry.getKey(); + if (imageReference.getRegistry().endsWith(registrySuffix)) { + String credentialHelper = entry.getValue(); + return Optional.of(retrieveFromDockerCredentialHelper(Paths.get(credentialHelper))); + } } catch (CredentialHelperNotFoundException | CredentialHelperUnhandledServerUrlException ex) { @@ -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 Google Application Default + * Credentials. + * + * @return a new {@link CredentialRetriever} + * @see https://cloud.google.com/docs/authentication/production + */ + 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 to be generated from the service account with + // refreshIfExpired() below 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. Setting GCS read-write scope")); + List 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 dockerConfigCredentials = - dockerConfigCredentialRetriever.retrieve(logger); - if (dockerConfigCredentials.isPresent()) { - logger.accept( - LogEvent.info( - "Using credentials from Docker config for " + imageReference.getRegistry())); - return dockerConfigCredentials; + Optional credentials = dockerConfigCredentialRetriever.retrieve(logger); + if (credentials.isPresent()) { + logGotCredentialsFrom("credentials from Docker config"); + return credentials; } } catch (IOException ex) { @@ -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; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java index 0e13727cde..968a5126c6 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java @@ -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; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java index b07cfc78ec..873d0ba339 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java @@ -40,8 +40,6 @@ */ public class DockerCredentialHelper { - public static final String CREDENTIAL_HELPER_PREFIX = "docker-credential-"; - private final String serverUrl; private final Path credentialHelper; @@ -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)); } /** diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java index eb05866a7f..efe28fb3fa 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java @@ -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; @@ -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(); } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java index 16537a52da..c01b3f4aef 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java @@ -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.ImageReference; import com.google.cloud.tools.jib.api.LogEvent; @@ -28,6 +30,7 @@ import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Collections; import java.util.Optional; import java.util.function.Consumer; import org.junit.Assert; @@ -44,98 +47,72 @@ public class CredentialRetrieverFactoryTest { private static final Credential FAKE_CREDENTIALS = Credential.from("username", "password"); - /** - * Returns a {@link DockerCredentialHelperFactory} that checks given parameters upon creating a - * {@link DockerCredentialHelper} instance. - * - * @param expectedRegistry the expected registry given to the factory - * @param expectedCredentialHelper the expected credential helper path given to the factory - * @param returnedCredentialHelper the mock credential helper to return - * @return a new {@link DockerCredentialHelperFactory} - */ - private static DockerCredentialHelperFactory getTestFactory( - String expectedRegistry, - Path expectedCredentialHelper, - DockerCredentialHelper returnedCredentialHelper) { - return (registry, credentialHelper) -> { - Assert.assertEquals(expectedRegistry, registry); - Assert.assertEquals(expectedCredentialHelper, credentialHelper); - return returnedCredentialHelper; - }; - } - @Mock private Consumer mockLogger; @Mock private DockerCredentialHelper mockDockerCredentialHelper; - @Mock private DockerConfigCredentialRetriever mockDockerConfigCredentialRetriever; - - /** A {@link DockerCredentialHelper} that throws {@link CredentialHelperNotFoundException}. */ - @Mock private DockerCredentialHelper mockNonexistentDockerCredentialHelper; - - @Mock private CredentialHelperNotFoundException mockCredentialHelperNotFoundException; + @Mock private DockerCredentialHelperFactory mockDockerCredentialHelperFactory; + @Mock private GoogleCredentials mockGoogleCredentials; @Before public void setUp() throws CredentialHelperUnhandledServerUrlException, CredentialHelperNotFoundException, IOException { + Mockito.when( + mockDockerCredentialHelperFactory.create(Mockito.anyString(), Mockito.any(Path.class))) + .thenReturn(mockDockerCredentialHelper); Mockito.when(mockDockerCredentialHelper.retrieve()).thenReturn(FAKE_CREDENTIALS); - Mockito.when(mockNonexistentDockerCredentialHelper.retrieve()) - .thenThrow(mockCredentialHelperNotFoundException); + Mockito.when(mockGoogleCredentials.getAccessToken()) + .thenReturn(new AccessToken("my-token", null)); } @Test public void testDockerCredentialHelper() throws CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = - new CredentialRetrieverFactory( - ImageReference.of("registry", "repository", null), - mockLogger, - getTestFactory( - "registry", Paths.get("docker-credential-helper"), mockDockerCredentialHelper)); + createCredentialRetrieverFactory("registry", "repository"); Assert.assertEquals( - FAKE_CREDENTIALS, + Optional.of(FAKE_CREDENTIALS), credentialRetrieverFactory .dockerCredentialHelper(Paths.get("docker-credential-helper")) - .retrieve() - .orElseThrow(AssertionError::new)); - Mockito.verify(mockLogger).accept(LogEvent.info("Using docker-credential-helper for registry")); + .retrieve()); + + Mockito.verify(mockDockerCredentialHelperFactory) + .create("registry", Paths.get("docker-credential-helper")); + Mockito.verify(mockLogger) + .accept(LogEvent.info("Using credentials from docker-credential-helper for registry")); } @Test - public void testInferCredentialHelper() throws CredentialRetrievalException { + public void testWellKnownCredentialHelpers() throws CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = - new CredentialRetrieverFactory( - ImageReference.of("something.gcr.io", "repository", null), - mockLogger, - getTestFactory( - "something.gcr.io", - Paths.get("docker-credential-gcr"), - mockDockerCredentialHelper)); + createCredentialRetrieverFactory("something.gcr.io", "repository"); Assert.assertEquals( - FAKE_CREDENTIALS, - credentialRetrieverFactory - .inferCredentialHelper() - .retrieve() - .orElseThrow(AssertionError::new)); + Optional.of(FAKE_CREDENTIALS), + credentialRetrieverFactory.wellKnownCredentialHelpers().retrieve()); + + Mockito.verify(mockDockerCredentialHelperFactory) + .create("something.gcr.io", Paths.get("docker-credential-gcr")); Mockito.verify(mockLogger) - .accept(LogEvent.info("Using docker-credential-gcr for something.gcr.io")); + .accept(LogEvent.info("Using credentials from docker-credential-gcr for something.gcr.io")); } @Test - public void testInferCredentialHelper_info() throws CredentialRetrievalException { + public void testWellKnownCredentialHelpers_info() + throws CredentialRetrievalException, IOException { + CredentialHelperNotFoundException notFoundException = + Mockito.mock(CredentialHelperNotFoundException.class); + Mockito.when(notFoundException.getMessage()).thenReturn("warning"); + Mockito.when(notFoundException.getCause()).thenReturn(new IOException("the root cause")); + Mockito.when(mockDockerCredentialHelper.retrieve()).thenThrow(notFoundException); + CredentialRetrieverFactory credentialRetrieverFactory = - new CredentialRetrieverFactory( - ImageReference.of("something.amazonaws.com", "repository", null), - mockLogger, - getTestFactory( - "something.amazonaws.com", - Paths.get("docker-credential-ecr-login"), - mockNonexistentDockerCredentialHelper)); - - Mockito.when(mockCredentialHelperNotFoundException.getMessage()).thenReturn("warning"); - Mockito.when(mockCredentialHelperNotFoundException.getCause()) - .thenReturn(new IOException("the root cause")); - Assert.assertFalse(credentialRetrieverFactory.inferCredentialHelper().retrieve().isPresent()); + createCredentialRetrieverFactory("something.amazonaws.com", "repository"); + + Assert.assertFalse( + credentialRetrieverFactory.wellKnownCredentialHelpers().retrieve().isPresent()); + + Mockito.verify(mockDockerCredentialHelperFactory) + .create("something.amazonaws.com", Paths.get("docker-credential-ecr-login")); Mockito.verify(mockLogger).accept(LogEvent.info("warning")); Mockito.verify(mockLogger).accept(LogEvent.info(" Caused by: the root cause")); } @@ -143,19 +120,121 @@ public void testInferCredentialHelper_info() throws CredentialRetrievalException @Test public void testDockerConfig() throws IOException, CredentialRetrievalException { CredentialRetrieverFactory credentialRetrieverFactory = - CredentialRetrieverFactory.forImage( - ImageReference.of("registry", "repository", null), mockLogger); + createCredentialRetrieverFactory("registry", "repository"); - Mockito.when(mockDockerConfigCredentialRetriever.retrieve(mockLogger)) + DockerConfigCredentialRetriever dockerConfigCredentialRetriever = + Mockito.mock(DockerConfigCredentialRetriever.class); + Mockito.when(dockerConfigCredentialRetriever.retrieve(mockLogger)) .thenReturn(Optional.of(FAKE_CREDENTIALS)); Assert.assertEquals( - FAKE_CREDENTIALS, - credentialRetrieverFactory - .dockerConfig(mockDockerConfigCredentialRetriever) - .retrieve() - .orElseThrow(AssertionError::new)); + Optional.of(FAKE_CREDENTIALS), + credentialRetrieverFactory.dockerConfig(dockerConfigCredentialRetriever).retrieve()); + Mockito.verify(mockLogger) .accept(LogEvent.info("Using credentials from Docker config for registry")); } + + @Test + public void testGoogleApplicationDefaultCredentials_notGoogleContainerRegistry() + throws CredentialRetrievalException { + CredentialRetrieverFactory credentialRetrieverFactory = + createCredentialRetrieverFactory("non.gcr.registry", "repository"); + + Assert.assertFalse( + credentialRetrieverFactory.googleApplicationDefaultCredentials().retrieve().isPresent()); + + Mockito.verifyZeroInteractions(mockLogger); + } + + @Test + public void testGoogleApplicationDefaultCredentials_adcNotPresent() + throws CredentialRetrievalException { + CredentialRetrieverFactory credentialRetrieverFactory = + new CredentialRetrieverFactory( + ImageReference.of("awesome.gcr.io", "repository", null), + mockLogger, + mockDockerCredentialHelperFactory, + () -> { + throw new IOException("ADC not present"); + }); + + Assert.assertFalse( + credentialRetrieverFactory.googleApplicationDefaultCredentials().retrieve().isPresent()); + + Mockito.verify(mockLogger) + .accept(LogEvent.info("ADC not present or error fetching access token: ADC not present")); + } + + @Test + public void testGoogleApplicationDefaultCredentials_refreshFailure() + throws CredentialRetrievalException, IOException { + Mockito.doThrow(new IOException("refresh failed")) + .when(mockGoogleCredentials) + .refreshIfExpired(); + + CredentialRetrieverFactory credentialRetrieverFactory = + createCredentialRetrieverFactory("awesome.gcr.io", "repository"); + + Assert.assertFalse( + credentialRetrieverFactory.googleApplicationDefaultCredentials().retrieve().isPresent()); + + Mockito.verify(mockLogger).accept(LogEvent.info("Google ADC found")); + Mockito.verify(mockLogger) + .accept(LogEvent.info("ADC not present or error fetching access token: refresh failed")); + Mockito.verifyNoMoreInteractions(mockLogger); + } + + @Test + public void testGoogleApplicationDefaultCredentials_endUserCredentials() + throws CredentialRetrievalException { + CredentialRetrieverFactory credentialRetrieverFactory = + createCredentialRetrieverFactory("awesome.gcr.io", "repository"); + + Credential credential = + credentialRetrieverFactory.googleApplicationDefaultCredentials().retrieve().get(); + Assert.assertEquals("oauth2accesstoken", credential.getUsername()); + Assert.assertEquals("my-token", credential.getPassword()); + + Mockito.verify(mockGoogleCredentials, Mockito.never()).createScoped(Mockito.anyString()); + + Mockito.verify(mockLogger).accept(LogEvent.info("Google ADC found")); + Mockito.verify(mockLogger) + .accept(LogEvent.info("Using Google Application Default Credentials for awesome.gcr.io")); + Mockito.verifyNoMoreInteractions(mockLogger); + } + + @Test + public void testGoogleApplicationDefaultCredentials_serviceAccount() + throws CredentialRetrievalException { + Mockito.when(mockGoogleCredentials.createScopedRequired()).thenReturn(true); + Mockito.when(mockGoogleCredentials.createScoped(Mockito.anyCollection())) + .thenReturn(mockGoogleCredentials); + + CredentialRetrieverFactory credentialRetrieverFactory = + createCredentialRetrieverFactory("gcr.io", "repository"); + + Credential credential = + credentialRetrieverFactory.googleApplicationDefaultCredentials().retrieve().get(); + Assert.assertEquals("oauth2accesstoken", credential.getUsername()); + Assert.assertEquals("my-token", credential.getPassword()); + + Mockito.verify(mockGoogleCredentials) + .createScoped( + Collections.singletonList("https://www.googleapis.com/auth/devstorage.read_write")); + + Mockito.verify(mockLogger).accept(LogEvent.info("Google ADC found")); + Mockito.verify(mockLogger) + .accept(LogEvent.info("ADC is a service account. Setting GCS read-write scope")); + Mockito.verify(mockLogger) + .accept(LogEvent.info("Using Google Application Default Credentials for gcr.io")); + Mockito.verifyNoMoreInteractions(mockLogger); + } + + private CredentialRetrieverFactory createCredentialRetrieverFactory( + String registry, String repository) { + ImageReference imageReference = ImageReference.of(registry, repository, null); + return new CredentialRetrieverFactory( + imageReference, mockLogger, mockDockerCredentialHelperFactory, () -> mockGoogleCredentials); + } } diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 4af7c7ad99..7424776e00 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. - Can now set timestamps (last modified time) of the files in the built image with `jib.container.filesModificationTime`. The value should either be `EPOCH_PLUS_SECOND` to set the timestamps to Epoch + 1 second (default behavior), or an ISO 8601 date time parsable with [`DateTimeFormatter.ISO_DATE_TIME`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html) such as `2019-07-15T10:15:30+09:00` or `2011-12-03T22:42:05Z` ([#1818](https://github.com/GoogleContainerTools/jib/pull/1818)) - Can now set container creation timestamp with `jib.container.creationTime`. The value should be `EPOCH`, `USE_CURRENT_TIMESTAMP`, or an ISO 8601 date time ([#1609](https://github.com/GoogleContainerTools/jib/issues/1609)) +- For Google Container Registry (gcr.io), Jib now tries [Google Application Default Credentials](https://developers.google.com/identity/protocols/application-default-credentials) (ADC) last when no credentials can be retrieved. ADC are available on many Google Cloud Platform (GCP) environments (such as Google Cloud Build, Google Compute Engine, Google Kubernetes Engine, and Google App Engine). Application Default Credentials can also be configured with `gcloud auth application-default login` locally or through the `GOOGLE_APPLICATION_CREDENTIALS` environment variable. ([#1902](https://github.com/GoogleContainerTools/jib/pull/1902)) ### Changed diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index 2bf0cd04b3..997d6073f9 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -63,8 +63,10 @@ dependencies { // These are copied over from jib-core and are necessary for the jib-core sourcesets. compile 'com.google.http-client:google-http-client:1.31.0' compile 'com.google.http-client:google-http-client-apache-v2:1.31.0' + compile 'com.google.auth:google-auth-library-oauth2-http:0.16.2' + compile 'com.google.guava:guava:28.0-jre' + compile 'org.apache.commons:commons-compress:1.18' - compile 'com.google.guava:guava:27.0.1-jre' compile 'com.fasterxml.jackson.core:jackson-databind:2.9.9.2' compile 'org.ow2.asm:asm:7.0' diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 70ec67d215..375d9db0ba 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. - Can now set file timestamps (last modified time) in the image with ``. The value should either be `EPOCH_PLUS_SECOND` to set the timestamps to Epoch + 1 second (default behavior), or an ISO 8601 date time parsable with [`DateTimeFormatter.ISO_DATE_TIME`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html) such as `2019-07-15T10:15:30+09:00` or `2011-12-03T22:42:05Z` ([#1818](https://github.com/GoogleContainerTools/jib/pull/1818)) - Can now set container creation timestamp with ``. The value should be `EPOCH`, `USE_CURRENT_TIMESTAMP`, or an ISO 8601 date time ([#1609](https://github.com/GoogleContainerTools/jib/issues/1609)) +- For Google Container Registry (gcr.io), Jib now tries [Google Application Default Credentials](https://developers.google.com/identity/protocols/application-default-credentials) (ADC) last when no credentials can be retrieved. ADC are available on many Google Cloud Platform (GCP) environments (such as Google Cloud Build, Google Compute Engine, Google Kubernetes Engine, and Google App Engine). Application Default Credentials can also be configured with `gcloud auth application-default login` locally or through the `GOOGLE_APPLICATION_CREDENTIALS` environment variable. ([#1902](https://github.com/GoogleContainerTools/jib/pull/1902)) ### Changed diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index e407b4f3d4..9bda66f030 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -63,15 +63,20 @@ compile - org.apache.commons - commons-compress - 1.18 - compile + com.google.auth + google-auth-library-oauth2-http + 0.16.2 com.google.guava guava - 27.0.1-jre + 28.0-jre + compile + + + org.apache.commons + commons-compress + 1.18 compile diff --git a/jib-plugins-common/build.gradle b/jib-plugins-common/build.gradle index 52330c3077..81ea47811f 100644 --- a/jib-plugins-common/build.gradle +++ b/jib-plugins-common/build.gradle @@ -31,8 +31,10 @@ dependencies { // Make sure these are consistent with jib-maven-plugin. compile 'com.google.http-client:google-http-client:1.31.0' compile 'com.google.http-client:google-http-client-apache-v2:1.31.0' + compile 'com.google.auth:google-auth-library-oauth2-http:0.16.2' + compile 'com.google.guava:guava:28.0-jre' + compile 'org.apache.commons:commons-compress:1.18' - compile 'com.google.guava:guava:27.0.1-jre' compile 'com.fasterxml.jackson.core:jackson-databind:2.9.9.2' compile 'org.ow2.asm:asm:7.0' diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java index 399d3cc2d6..97b6633d7f 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrievers.java @@ -19,7 +19,6 @@ import com.google.cloud.tools.jib.api.Credential; import com.google.cloud.tools.jib.api.CredentialRetriever; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; -import com.google.cloud.tools.jib.registry.credentials.DockerCredentialHelper; import java.io.FileNotFoundException; import java.nio.file.FileSystems; import java.nio.file.Files; @@ -34,11 +33,14 @@ *

The retrievers are, in order of first-checked to last-checked: * *

    + *
  1. {@link CredentialRetrieverFactory#known} for known credential, if set *
  2. {@link CredentialRetrieverFactory#dockerCredentialHelper} for a known credential helper, if * set - *
  3. {@link CredentialRetrieverFactory#known} for known credential, if set - *
  4. {@link CredentialRetrieverFactory#inferCredentialHelper} + *
  5. {@link CredentialRetrieverFactory#known} for known inferred credential, if set *
  6. {@link CredentialRetrieverFactory#dockerConfig} + *
  7. {@link CredentialRetrieverFactory#wellKnownCredentialHelpers} for well-known credential + * helper-registry pairs + *
  8. {@link CredentialRetrieverFactory#googleApplicationDefaultCredentials} for GCR registry *
*/ public class DefaultCredentialRetrievers { @@ -121,24 +123,24 @@ public List asList() throws FileNotFoundException { if (credentialHelper != null) { // If credential helper contains file separator, treat as path; otherwise treat as suffix if (credentialHelper.contains(FileSystems.getDefault().getSeparator())) { - if (Files.exists(Paths.get(credentialHelper))) { - credentialRetrievers.add( - credentialRetrieverFactory.dockerCredentialHelper(credentialHelper)); - } else { + if (!Files.exists(Paths.get(credentialHelper))) { throw new FileNotFoundException( "Specified credential helper was not found: " + credentialHelper); } + credentialRetrievers.add( + credentialRetrieverFactory.dockerCredentialHelper(credentialHelper)); } else { + String suffix = credentialHelper; // not path; treat as suffix credentialRetrievers.add( - credentialRetrieverFactory.dockerCredentialHelper( - DockerCredentialHelper.CREDENTIAL_HELPER_PREFIX + credentialHelper)); + credentialRetrieverFactory.dockerCredentialHelper("docker-credential-" + suffix)); } } if (inferredCredentialRetriever != null) { credentialRetrievers.add(inferredCredentialRetriever); } credentialRetrievers.add(credentialRetrieverFactory.dockerConfig()); - credentialRetrievers.add(credentialRetrieverFactory.inferCredentialHelper()); + credentialRetrievers.add(credentialRetrieverFactory.wellKnownCredentialHelpers()); + credentialRetrievers.add(credentialRetrieverFactory.googleApplicationDefaultCredentials()); return credentialRetrievers; } } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java index 2d2ac99e3c..1d589ab43b 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/DefaultCredentialRetrieversTest.java @@ -39,14 +39,15 @@ @RunWith(MockitoJUnitRunner.class) public class DefaultCredentialRetrieversTest { - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Mock private CredentialRetrieverFactory mockCredentialRetrieverFactory; @Mock private CredentialRetriever mockDockerCredentialHelperCredentialRetriever; @Mock private CredentialRetriever mockKnownCredentialRetriever; @Mock private CredentialRetriever mockInferredCredentialRetriever; - @Mock private CredentialRetriever mockInferCredentialHelperCredentialRetriever; + @Mock private CredentialRetriever mockWellKnownCredentialHelpersCredentialRetriever; @Mock private CredentialRetriever mockDockerConfigCredentialRetriever; + @Mock private CredentialRetriever mockApplicationDefaultCredentialRetriever; private final Credential knownCredential = Credential.from("username", "password"); private final Credential inferredCredential = Credential.from("username2", "password2"); @@ -60,10 +61,12 @@ public void setUp() { Mockito.when( mockCredentialRetrieverFactory.known(inferredCredential, "inferredCredentialSource")) .thenReturn(mockInferredCredentialRetriever); - Mockito.when(mockCredentialRetrieverFactory.inferCredentialHelper()) - .thenReturn(mockInferCredentialHelperCredentialRetriever); + Mockito.when(mockCredentialRetrieverFactory.wellKnownCredentialHelpers()) + .thenReturn(mockWellKnownCredentialHelpersCredentialRetriever); Mockito.when(mockCredentialRetrieverFactory.dockerConfig()) .thenReturn(mockDockerConfigCredentialRetriever); + Mockito.when(mockCredentialRetrieverFactory.googleApplicationDefaultCredentials()) + .thenReturn(mockApplicationDefaultCredentialRetriever); } @Test @@ -72,7 +75,9 @@ public void testInitAsList() throws FileNotFoundException { DefaultCredentialRetrievers.init(mockCredentialRetrieverFactory).asList(); Assert.assertEquals( Arrays.asList( - mockDockerConfigCredentialRetriever, mockInferCredentialHelperCredentialRetriever), + mockDockerConfigCredentialRetriever, + mockWellKnownCredentialHelpersCredentialRetriever, + mockApplicationDefaultCredentialRetriever), credentialRetrievers); } @@ -90,7 +95,8 @@ public void testInitAsList_all() throws FileNotFoundException { mockDockerCredentialHelperCredentialRetriever, mockInferredCredentialRetriever, mockDockerConfigCredentialRetriever, - mockInferCredentialHelperCredentialRetriever), + mockWellKnownCredentialHelpersCredentialRetriever, + mockApplicationDefaultCredentialRetriever), credentialRetrievers); Mockito.verify(mockCredentialRetrieverFactory).known(knownCredential, "credentialSource"); @@ -112,7 +118,8 @@ public void testInitAsList_credentialHelperPath() throws IOException { Arrays.asList( mockDockerCredentialHelperCredentialRetriever, mockDockerConfigCredentialRetriever, - mockInferCredentialHelperCredentialRetriever), + mockWellKnownCredentialHelpersCredentialRetriever, + mockApplicationDefaultCredentialRetriever), credentialRetrievers); Mockito.verify(mockCredentialRetrieverFactory) .dockerCredentialHelper(fakeCredentialHelperPath.toString());