blob: e8a720c4c365160b1deaf87690836ef72d902f03 [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* 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.android.tradefed.util;
import com.android.tradefed.config.GlobalConfiguration;
import com.android.tradefed.host.HostOptions;
import com.android.tradefed.log.LogUtil.CLog;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpUnsuccessfulResponseHandler;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.util.ExponentialBackOff;
import com.google.common.annotations.VisibleForTesting;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/** Utils for create Google API client. */
public class GoogleApiClientUtil {
public static final String APP_NAME = "tradefed";
private static GoogleApiClientUtil sInstance = null;
private static GoogleApiClientUtil getInstance() {
if (sInstance == null) {
sInstance = new GoogleApiClientUtil();
}
return sInstance;
}
/**
* Create credential from json key file.
*
* @param file is the p12 key file
* @param scopes is the API's scope.
* @return a {@link GoogleCredential}.
* @throws FileNotFoundException
* @throws IOException
* @throws GeneralSecurityException
*/
public static GoogleCredential createCredentialFromJsonKeyFile(
File file, Collection<String> scopes) throws IOException, GeneralSecurityException {
return getInstance().doCreateCredentialFromJsonKeyFile(file, scopes);
}
@VisibleForTesting
GoogleCredential doCreateCredentialFromJsonKeyFile(File file, Collection<String> scopes)
throws IOException, GeneralSecurityException {
return GoogleCredential.fromStream(
new FileInputStream(file),
GoogleNetHttpTransport.newTrustedTransport(),
JacksonFactory.getDefaultInstance())
.createScoped(scopes);
}
/**
* Try to create credential with different key files or from local host.
*
* <p>1. If primaryKeyFile is set, try to use it to create credential. 2. Try to get
* corresponding key files from {@link HostOptions}. 3. Try to use backup key files. 4. Use
* local default credential.
*
* @param scopes scopes for the credential.
* @param primaryKeyFile the primary json key file; it can be null.
* @param hostOptionKeyFileName {@link HostOptions}'service-account-json-key-file option's key;
* it can be null.
* @param backupKeyFiles backup key files.
* @return a {@link GoogleCredential}
* @throws IOException
* @throws GeneralSecurityException
*/
public static GoogleCredential createCredential(
Collection<String> scopes,
File primaryKeyFile,
String hostOptionKeyFileName,
File... backupKeyFiles)
throws IOException, GeneralSecurityException {
return getInstance()
.doCreateCredential(scopes, primaryKeyFile, hostOptionKeyFileName, backupKeyFiles);
}
@VisibleForTesting
GoogleCredential doCreateCredential(
Collection<String> scopes,
File primaryKeyFile,
String hostOptionKeyFileName,
File... backupKeyFiles)
throws IOException, GeneralSecurityException {
List<File> keyFiles = new ArrayList<File>();
if (primaryKeyFile != null) {
keyFiles.add(primaryKeyFile);
}
File hostOptionKeyFile = null;
if (hostOptionKeyFileName != null) {
try {
hostOptionKeyFile =
GlobalConfiguration.getInstance()
.getHostOptions()
.getServiceAccountJsonKeyFiles()
.get(hostOptionKeyFileName);
if (hostOptionKeyFile != null) {
keyFiles.add(hostOptionKeyFile);
}
} catch (IllegalStateException e) {
CLog.d("Global configuration haven't been initialized.");
}
}
keyFiles.addAll(Arrays.asList(backupKeyFiles));
for (File keyFile : keyFiles) {
if (keyFile != null) {
if (keyFile.exists() && keyFile.canRead()) {
CLog.d("Using %s.", keyFile.getAbsolutePath());
return doCreateCredentialFromJsonKeyFile(keyFile, scopes);
} else {
CLog.i("No access to %s.", keyFile.getAbsolutePath());
}
}
}
return doCreateDefaultCredential(scopes);
}
@VisibleForTesting
GoogleCredential doCreateDefaultCredential(Collection<String> scopes) throws IOException {
try {
CLog.d("Using local authentication.");
return GoogleCredential.getApplicationDefault().createScoped(scopes);
} catch (IOException e) {
CLog.e(
"Try 'gcloud auth application-default login' to login for "
+ "personal account; Or 'export "
+ "GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json' "
+ "for service account.");
throw e;
}
}
/**
* Create credential from p12 file for service account.
*
* @deprecated It's better to use json key file, since p12 is deprecated by Google App Engine.
* And json key file have more information.
* @param serviceAccount is the service account
* @param keyFile is the p12 key file
* @param scopes is the API's scope.
* @return a {@link GoogleCredential}.
* @throws GeneralSecurityException
* @throws IOException
*/
@Deprecated
public static GoogleCredential createCredentialFromP12File(
String serviceAccount, File keyFile, Collection<String> scopes)
throws GeneralSecurityException, IOException {
return new GoogleCredential.Builder()
.setTransport(GoogleNetHttpTransport.newTrustedTransport())
.setJsonFactory(JacksonFactory.getDefaultInstance())
.setServiceAccountId(serviceAccount)
.setServiceAccountScopes(scopes)
.setServiceAccountPrivateKeyFromP12File(keyFile)
.build();
}
/**
* @param requestInitializer a {@link HttpRequestInitializer}, normally it's {@link
* GoogleCredential}.
* @param connectTimeout connect timeout in milliseconds.
* @param readTimeout read timeout in milliseconds.
* @return a {@link HttpRequestInitializer} with timeout.
*/
public static HttpRequestInitializer setHttpTimeout(
final HttpRequestInitializer requestInitializer, int connectTimeout, int readTimeout) {
return new HttpRequestInitializer() {
@Override
public void initialize(HttpRequest request) throws IOException {
requestInitializer.initialize(request);
request.setConnectTimeout(connectTimeout);
request.setReadTimeout(readTimeout);
}
};
}
/**
* Setup a retry strategy for the provided HttpRequestInitializer. In case of server errors
* requests will be automatically retried with an exponential backoff.
*
* @param initializer - an initializer which will setup a retry strategy.
* @return an initializer that will retry failed requests automatically.
*/
public static HttpRequestInitializer configureRetryStrategy(
HttpRequestInitializer initializer) {
return new HttpRequestInitializer() {
@Override
public void initialize(HttpRequest request) throws IOException {
initializer.initialize(request);
request.setUnsuccessfulResponseHandler(new RetryResponseHandler());
}
};
}
private static class RetryResponseHandler implements HttpUnsuccessfulResponseHandler {
// Initial interval to wait before retrying if a request fails.
private static final int INITIAL_RETRY_INTERVAL = 1000;
private final HttpUnsuccessfulResponseHandler backOffHandler;
public RetryResponseHandler() {
backOffHandler =
new HttpBackOffUnsuccessfulResponseHandler(
new ExponentialBackOff.Builder()
.setInitialIntervalMillis(INITIAL_RETRY_INTERVAL)
.build());
}
@Override
public boolean handleResponse(
HttpRequest request, HttpResponse response, boolean supportsRetry)
throws IOException {
CLog.w(
"Request to %s failed: %d %s",
request.getUrl(), response.getStatusCode(), response.getStatusMessage());
return backOffHandler.handleResponse(request, response, supportsRetry);
}
}
}