org.mandas.docker.client.auth.gcr.ContainerRegistryAuthSupplier Maven / Gradle / Ivy
/*-
* -\-\-
* docker-client
* --
* Copyright (C) 2016 - 2017 Spotify AB
* Copyright (C) 9/2019 - 2020 Dimitris Mandalidis
* --
* 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 org.mandas.docker.client.auth.gcr;
import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableSet;
import java.io.IOException;
import java.io.InputStream;
import java.time.Clock;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.mandas.docker.client.auth.RegistryAuthSupplier;
import org.mandas.docker.client.exceptions.DockerException;
import org.mandas.docker.client.messages.RegistryAuth;
import org.mandas.docker.client.messages.RegistryConfigs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.ServiceAccountCredentials;
import com.google.auth.oauth2.UserCredentials;
/**
* 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 = unmodifiableSet(new HashSet<>(asList(
"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.
* @param credentialsStream the credentials stream
* @return a new {@link Builder}
* @throws IOException when an error occured when reading from stream
*
* @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.
* @return a new {@link Builder}
* @throws IOException when an error occured when reading from stream
* @see Builder
*/
public static Builder forApplicationDefaultCredentials() throws IOException {
return new Builder(GoogleCredentials.getApplicationDefault());
}
/**
* Constructs a ContainerRegistryAuthSupplier using the specified credentials.
* @param credentials the google credentials
* @return a new {@link Builder}
*
* @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 =
Collections.singletonList("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
* @param scopes a list of scopes
* @return this
*/
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.
* @param duration the expiry duration value
* @param timeUnit the expiry duration time unit
* @return this
*/
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.
*/
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;
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);
}
}