All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.spotify.docker.client.auth.gcr.ContainerRegistryAuthSupplier Maven / Gradle / Ivy

There is a newer version: 8.16.0
Show newest version
/*-
 * -\-\-
 * docker-client
 * --
 * Copyright (C) 2016 - 2017 Spotify AB
 * --
 * 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.spotify.docker.client.auth.gcr;

import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.ServiceAccountCredentials;
import com.google.auth.oauth2.UserCredentials;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.spotify.docker.client.auth.RegistryAuthSupplier;
import com.spotify.docker.client.exceptions.DockerException;
import com.spotify.docker.client.messages.RegistryAuth;
import com.spotify.docker.client.messages.RegistryConfigs;
import java.io.IOException;
import java.io.InputStream;
import java.time.Clock;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A RegistryAuthSupplier for getting access tokens from a Google Cloud Platform service or user
 * account. This implementation uses the google-auth-library-oauth2-http library to get an access
 * token given an account's credentials, and will refresh the token if it expires within a
 * configurable amount of time.
 * 

* To construct a new instance, use * {@link ContainerRegistryAuthSupplier#fromStream(InputStream)} to get a {@link Builder} if you * have credentials for the account to use, or you can use * {@link ContainerRegistryAuthSupplier#forApplicationDefaultCredentials()} * if you would like to use * the * Application Default Credentials.

*

* The scopes used to fetch an access token and the minimum expiry time can be configured via the * Builder before calling {@link Builder#build()}.

*/ public class ContainerRegistryAuthSupplier implements RegistryAuthSupplier { private static final Logger log = LoggerFactory.getLogger(ContainerRegistryAuthSupplier.class); // the list returned by `gcloud docker -a` // this may change in the future, and we can't know all values - but should cover most use cases private static final Set GCR_REGISTRIES = ImmutableSet.of( "gcr.io", "us.gcr.io", "eu.gcr.io", "asia.gcr.io", "b.gcr.io", "bucket.gcr.io", "l.gcr.io", "launcher.gcr.io", "appengine.gcr.io", "us-mirror.gcr.io", "eu-mirror.gcr.io", "asia-mirror.gcr.io", "mirror.gcr.io" ); /** * Constructs a ContainerRegistryAuthSupplier for the account with the given credentials. * * @see Builder */ public static Builder fromStream(final InputStream credentialsStream) throws IOException { final GoogleCredentials credentials = GoogleCredentials.fromStream(credentialsStream); return new Builder(credentials); } /** * Constructs a ContainerRegistryAuthSupplier using the Application Default Credentials. * * @see Builder */ public static Builder forApplicationDefaultCredentials() throws IOException { return new Builder(GoogleCredentials.getApplicationDefault()); } /** * Constructs a ContainerRegistryAuthSupplier using the specified credentials. * * @see Builder */ public static Builder forCredentials(final GoogleCredentials credentials) { return new Builder(credentials); } /** * A Builder of ContainerRegistryAuthSupplier. *

* The access tokens returned by the ContainerRegistryAuthSupplier are scoped to * devstorage.read_write by default, these can be customized with {@link #withScopes(Collection)}. *

*

* The default value for the minimum expiry time of an access token is one minute. When the * ContainerRegistryAuthSupplier is asked for a RegistryAuth, it will check if the existing * AccessToken for the GoogleCredentials expires within this amount of time. If it does, then the * AccessToken is refreshed before being returned.

*/ public static class Builder { private final GoogleCredentials credentials; private Collection scopes = ImmutableList.of("https://www.googleapis.com/auth/devstorage.read_write"); private long minimumExpiryMillis = TimeUnit.MINUTES.toMillis(1); public Builder(final GoogleCredentials credentials) { this.credentials = credentials; } /** * Changes the scopes * used in fetching AccessTokens. *

* The default is devstorage.read_write, which allows pulling and pushing of images. * To allow the application using ContainerRegistryAuthSupplier to pull but not push images, * change the scope to contain only devstorage.read_only. * only

*/ public Builder withScopes(Collection scopes) { this.scopes = scopes; return this; } /** * Changes the minimum expiry time used to refresh AccessTokens before they expire. The * default value is one minute. */ public Builder withMinimumExpiry(long duration, TimeUnit timeUnit) { this.minimumExpiryMillis = TimeUnit.MILLISECONDS.convert(duration, timeUnit); return this; } public ContainerRegistryAuthSupplier build() { final GoogleCredentials credentials = this.credentials.createScoped(scopes); // log some sort of identifier for the credentials, which requires looking at the // instance type if (credentials instanceof ServiceAccountCredentials) { final String clientEmail = ((ServiceAccountCredentials) credentials).getClientEmail(); log.info("loaded credentials for service account with clientEmail={}", clientEmail); } else if (credentials instanceof UserCredentials) { final String clientId = ((UserCredentials) credentials).getClientId(); log.info("loaded credentials for user account with clientId={}", clientId); } final Clock clock = Clock.systemDefaultZone(); final DefaultCredentialRefresher refresher = new DefaultCredentialRefresher(); return new ContainerRegistryAuthSupplier(credentials, clock, minimumExpiryMillis, refresher); } } /** * Refreshes a GoogleCredentials instance. This only exists for testing - we cannot mock calls to * {@link GoogleCredentials#refresh()} as the method is final. */ @VisibleForTesting interface CredentialRefresher { void refresh(GoogleCredentials credentials) throws IOException; } private static class DefaultCredentialRefresher implements CredentialRefresher { @Override public void refresh(final GoogleCredentials credentials) throws IOException { credentials.refresh(); } } private final GoogleCredentials credentials; private final Clock clock; private final long minimumExpiryMillis; private final CredentialRefresher credentialRefresher; @VisibleForTesting ContainerRegistryAuthSupplier( final GoogleCredentials credentials, final Clock clock, final long minimumExpiryMillis, final CredentialRefresher credentialRefresher) { this.credentials = credentials; this.clock = clock; this.minimumExpiryMillis = minimumExpiryMillis; this.credentialRefresher = credentialRefresher; } /** * Get an accessToken to use, possibly refreshing the token if it expires within the * minimumExpiryMillis. */ private AccessToken getAccessToken() throws IOException { // synchronize attempts to refresh the accessToken synchronized (credentials) { if (needsRefresh(credentials.getAccessToken())) { credentialRefresher.refresh(credentials); } } return credentials.getAccessToken(); } private boolean needsRefresh(final AccessToken accessToken) { if (accessToken == null) { // has not yet been fetched return true; } final Date expirationTime = credentials.getAccessToken().getExpirationTime(); // Don't refresh if expiration time hasn't been provided. if (expirationTime == null) { return false; } // refresh the token if it expires "soon" final long expiresIn = expirationTime.getTime() - clock.millis(); return expiresIn <= minimumExpiryMillis; } @Override public RegistryAuth authFor(final String imageName) throws DockerException { final String[] imageParts = imageName.split("/", 2); if (imageParts.length < 2 || !GCR_REGISTRIES.contains(imageParts[0])) { // not an image on GCR return null; } final AccessToken accessToken; try { accessToken = getAccessToken(); } catch (IOException e) { throw new DockerException(e); } return authForAccessToken(accessToken); } // see https://cloud.google.com/container-registry/docs/advanced-authentication private RegistryAuth authForAccessToken(final AccessToken accessToken) { return RegistryAuth.builder() .username("oauth2accesstoken") .password(accessToken.getTokenValue()) .build(); } @Override public RegistryAuth authForSwarm() throws DockerException { final AccessToken accessToken; try { accessToken = getAccessToken(); } catch (IOException e) { // ignore the exception, as the user may not care if swarm is authenticated to use GCR log.warn("unable to get access token for Google Container Registry due to exception, " + "configuration for Swarm will not contain RegistryAuth for GCR", e); return null; } return authForAccessToken(accessToken); } @Override public RegistryConfigs authForBuild() throws DockerException { final AccessToken accessToken; try { accessToken = getAccessToken(); } catch (IOException e) { // do not fail as the GCR access token may not be necessary for building the image currently // being built log.warn("unable to get access token for Google Container Registry, " + "configuration for building image will not contain RegistryAuth for GCR", e); return RegistryConfigs.empty(); } final Map configs = new HashMap<>(GCR_REGISTRIES.size()); for (String serverName : GCR_REGISTRIES) { configs.put(serverName, authForAccessToken(accessToken)); } return RegistryConfigs.create(configs); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy