/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you 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.elasticsearch.repositories.gcs; import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; import com.google.api.client.http.HttpBackOffIOExceptionHandler; import com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler; import com.google.api.client.http.HttpIOExceptionHandler; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestInitializer; import com.google.api.client.http.HttpUnsuccessfulResponseHandler; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.jackson2.JacksonFactory; import com.google.api.client.util.ExponentialBackOff; import com.google.api.services.storage.Storage; import com.google.api.services.storage.StorageScopes; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.component.AbstractComponent; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.env.Environment; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; interface GoogleCloudStorageService { /** * Creates a client that can be used to manage Google Cloud Storage objects. * * @param serviceAccount path to service account file * @param application name of the application * @param connectTimeout connection timeout for HTTP requests * @param readTimeout read timeout for HTTP requests * @return a Client instance that can be used to manage objects */ Storage createClient(String serviceAccount, String application, TimeValue connectTimeout, TimeValue readTimeout) throws Exception; /** * Default implementation */ class InternalGoogleCloudStorageService extends AbstractComponent implements GoogleCloudStorageService { private static final String DEFAULT = "_default_"; private final Environment environment; InternalGoogleCloudStorageService(Environment environment) { super(environment.settings()); this.environment = environment; } @Override public Storage createClient(String serviceAccount, String application, TimeValue connectTimeout, TimeValue readTimeout) throws Exception { try { GoogleCredential credentials = (DEFAULT.equalsIgnoreCase(serviceAccount)) ? loadDefault() : loadCredentials(serviceAccount); NetHttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport(); Storage.Builder storage = new Storage.Builder(httpTransport, JacksonFactory.getDefaultInstance(), new DefaultHttpRequestInitializer(credentials, connectTimeout, readTimeout)); storage.setApplicationName(application); logger.debug("initializing client with service account [{}/{}]", credentials.getServiceAccountId(), credentials.getServiceAccountUser()); return storage.build(); } catch (IOException e) { throw new ElasticsearchException("Error when loading Google Cloud Storage credentials file", e); } } /** * HTTP request initializer that loads credentials from the service account file * and manages authentication for HTTP requests */ private GoogleCredential loadCredentials(String serviceAccount) throws IOException { if (serviceAccount == null) { throw new ElasticsearchException("Cannot load Google Cloud Storage service account file from a null path"); } Path account = environment.configFile().resolve(serviceAccount); if (Files.exists(account) == false) { throw new ElasticsearchException("Unable to find service account file [" + serviceAccount + "] defined for repository"); } try (InputStream is = Files.newInputStream(account)) { GoogleCredential credential = GoogleCredential.fromStream(is); if (credential.createScopedRequired()) { credential = credential.createScoped(Collections.singleton(StorageScopes.DEVSTORAGE_FULL_CONTROL)); } return credential; } } /** * HTTP request initializer that loads default credentials when running on Compute Engine */ private GoogleCredential loadDefault() throws IOException { return GoogleCredential.getApplicationDefault(); } /** * HTTP request initializer that set timeouts and backoff handler while deferring authentication to GoogleCredential. * See https://cloud.google.com/storage/transfer/create-client#retry */ class DefaultHttpRequestInitializer implements HttpRequestInitializer { private final TimeValue connectTimeout; private final TimeValue readTimeout; private final GoogleCredential credential; private final HttpUnsuccessfulResponseHandler handler; private final HttpIOExceptionHandler ioHandler; DefaultHttpRequestInitializer(GoogleCredential credential, TimeValue connectTimeout, TimeValue readTimeout) { this.credential = credential; this.connectTimeout = connectTimeout; this.readTimeout = readTimeout; this.handler = new HttpBackOffUnsuccessfulResponseHandler(newBackOff()); this.ioHandler = new HttpBackOffIOExceptionHandler(newBackOff()); } @Override public void initialize(HttpRequest request) throws IOException { if (connectTimeout != null) { request.setConnectTimeout((int) connectTimeout.millis()); } if (readTimeout != null) { request.setReadTimeout((int) readTimeout.millis()); } request.setIOExceptionHandler(ioHandler); request.setInterceptor(credential); request.setUnsuccessfulResponseHandler((req, resp, supportsRetry) -> { // Let the credential handle the response. If it failed, we rely on our backoff handler return credential.handleResponse(req, resp, supportsRetry) || handler.handleResponse(req, resp, supportsRetry); } ); } private ExponentialBackOff newBackOff() { return new ExponentialBackOff.Builder() .setInitialIntervalMillis(100) .setMaxIntervalMillis(6000) .setMaxElapsedTimeMillis(900000) .setMultiplier(1.5) .setRandomizationFactor(0.5) .build(); } } } }